I am building a UITableView that is going to have cells with different layouts in them. The cell I am having issues with has a UICollectionView embedded in it that is generated from an API.
The category name and id populate in the cell correctly, but the images in the UICollectionView do not. The images load, but they are not the right ones for that category. Screen capture of how the collection is loading currently
Some of the things I've tried:
Hard-coding the ids for each one of the categories instead of dynamically generating them. When I do this, the correct images load (sometimes but not always) ... and if they do load correctly, when I scroll the images change to wrong ones
The prepareForReuse() function ... I'm not exactly sure where I would put it and what I would reset in it (I have code I believe already kind of nils the image out [code included below])
I have spent a few hours trying to figure this out, but I am stuck ... any suggestions would be appreciated.
My View Controller:
class EcardsViewController: BaseViewController {
#IBOutlet weak var categoryTable: UITableView!
var categories = [CategoryItem]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.categoryTable.dataSource! = self
self.categoryTable.delegate! = self
DispatchQueue.main.async {
let jsonUrlString = "https://*********/******/category"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
if err == nil {
do {
let decoder = JSONDecoder()
let ecardcategory = try decoder.decode(Category.self, from: data)
self.categories = ecardcategory.category
self.categories.sort(by: {$0.title < $1.title})
self.categories = self.categories.filter{$0.isFeatured}
} catch let err {
print("Err", err)
}
DispatchQueue.main.async {
self.categoryTable.reloadData()
}
}
}.resume()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension EcardsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categories.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "EcardsCategoriesTableViewCell", for: indexPath) as! EcardsCategoriesTableViewCell
cell.categoryName.text = ("\(categories[indexPath.row].title)**\(categories[indexPath.row].id)")
cell.ecardCatId = String(categories[indexPath.row].id)
return cell
}
}
My Table Cell:
class EcardsCategoriesTableViewCell: UITableViewCell {
#IBOutlet weak var categoryName: UILabel!
#IBOutlet weak var thisEcardCollection: UICollectionView!
var ecardCatId = ""
var theseEcards = [Content]()
let imageCache = NSCache<NSString,AnyObject>()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.thisEcardCollection.dataSource! = self
self.thisEcardCollection.delegate! = self
DispatchQueue.main.async {
let jsonUrlString = "https://**********/*******/content?category=\(self.ecardCatId)"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
if err == nil {
do {
let decoder = JSONDecoder()
let ecards = try decoder.decode(Ecards.self, from: data)
self.theseEcards = ecards.content
self.theseEcards = self.theseEcards.filter{$0.isActive}
} catch let err {
print("Err", err)
}
DispatchQueue.main.async {
self.thisEcardCollection.reloadData()
}
}
}.resume()
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
extension EcardsCategoriesTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return theseEcards.count > 7 ? 7 : theseEcards.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "EcardCategoriesCollectionViewCell", for: indexPath) as! EcardCategoriesCollectionViewCell
cell.ecardImage.contentMode = .scaleAspectFill
let ecardImageLink = theseEcards[indexPath.row].thumbSSL
cell.ecardImage.downloadedFrom(link: ecardImageLink)
return cell
}
}
Collection View Cell:
class EcardCategoriesCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var ecardImage: UIImageView!
}
Extension to "download" image:
extension UIImageView {
func downloadedFromReset(url: URL, contentMode mode: UIViewContentMode = .scaleAspectFit, thisurl: String) {
contentMode = mode
self.image = nil
// check cache
if let cachedImage = ImageCache.shared.image(forKey: thisurl) {
self.image = cachedImage
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else { return }
ImageCache.shared.save(image: image, forKey: thisurl)
DispatchQueue.main.async() {
self.image = image
}
}.resume()
}
func downloadedFrom(link: String, contentMode mode: UIViewContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloadedFromReset(url: url, contentMode: mode, thisurl: link)
}
}
Both UICollectionViewCell and UITableViewCell are reused. As one scrolls off the top of the screen, it is reinserted below the visible cells as the next cell that will appear on screen. The cells retain any data that they have during this dequeuing/requeuing process. prepareForReuse exists to give you a point to reset the view to default values and to clear any data from the last time it was displayed. This is especially important when working with asynchronous processes, such as network calls, as they can outlive the amount of time that a cell is displayed. Additionally, you're doing a lot of non-setup work in awakeFromNib. This method is not called every time a cell is displayed, it is only called the FIRST time a cell is displayed. If that cell goes off screen and is reused, awakeFromNib is not called. This is likely a big reason that your collection views have the wrong data, they're never making their network request when they appear on screen.
EcardsCategoriesTableViewCell:
prepareForReuse should be implemented. A few things need to occur in this method:
theseEcards should be nilled. When a table view scrolls off screen, you want to get rid of the collection view data or else the next time that cell is displayed, it will show the collection view data potentially for the wrong cell.
You should keep a reference to the dataTask that runs in awakeFromNib and then call cancel on this dataTask in prepareForReuse. Without doing this, the cell can display, disappear, then get reused before the dataTask completes. If that is the case, it may replace the intended values with the values from the previous dataTask (the one that was supposed to run on the cell that was scrolled off screen).
Additionally, the network call needs to be moved out of awakeFromNib:
You are only ever making the network call in awakeFromNib. This method only gets called the first time a cell is created. When you reuse a cell, it is not called. This method should be used to do any additional setup of views from the nib, but is not your main entry point in adding data to a cell. I would add a method on your cell that lets you set the category id. This will make the network request. It will look something like this:
func setCategoryId(_ categoryId: String) {
DispatchQueue.main.async {
let jsonUrlString = "https://**********/*******/content?category=\(categoryId)"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
if err == nil {
do {
let decoder = JSONDecoder()
let ecards = try decoder.decode(Ecards.self, from: data)
self.theseEcards = ecards.content
self.theseEcards = self.theseEcards.filter{$0.isActive}
} catch let err {
print("Err", err)
}
DispatchQueue.main.async {
self.thisEcardCollection.reloadData()
}
}
}.resume()
}
}
This will be called in the cellForRowAt dataSource method in EcardsViewController.
EcardCategoriesCollectionViewCell:
This cell has similar issues. You are setting images asynchronously, but are not clearing the images and cancelling the network requests when the cell is going to be reused. prepareForReuse should be implemented and the following should occur within it:
The image on the image view should be cleared or set to a default image.
The image request should be cancelled. This is going to take some refactoring to accomplish. You need to hold a reference to the dataTask in the collection view cell so that you can cancel it when appropriate.
After implementing these changes in the cells, you'll likely notice that the tableview and collection view feel slow. Data isn't instantly available. You'll want to cache the data or preload it some way. That is a bigger discussion than is right for this thread, but it will be your next step.
Related
I have a UITableView and during the initial loading of my app it sends multiple API requests. As each API request returns, I add a new row to the UITableView. So the initial loading adds rows in random orders at random times (Mostly it all happens within a second).
During cell setup, I call an Async method to generate an MKMapKit MKMapSnapshotter image.
I've used async image loading before without issue, but very rarely I end up with the image in the wrong cell and I can't figure out why.
I've tried switching to DiffableDataSource but the problem remains.
In my DiffableDataSource I pass a closure to the cell that is called when the image async returns, to fetch the current cell in case it's changed:
let dataSource = DiffableDataSource(tableView: tableView) {
(tableView, indexPath, journey) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "busCell", for: indexPath) as! JourneyTableViewCell
cell.setupCell(for: journey) { [weak self] () -> (cell: JourneyTableViewCell?, journey: Journey?) in
if let self = self
{
let cell = tableView.cellForRow(at: indexPath) as? JourneyTableViewCell
let journey = self.sortedJourneys()[safe: indexPath.section]
return (cell, journey)
}
return(nil, nil)
}
return cell
}
Here's my cell setup code:
override func prepareForReuse() {
super.prepareForReuse()
setMapImage(nil)
journey = nil
asyncCellBlock = nil
}
func setupCell(for journey:Journey, asyncUpdateOriginalCell:#escaping JourneyOriginalCellBlock) {
self.journey = journey
// Store the async block for later
asyncCellBlock = asyncUpdateOriginalCell
// Map
if let location = journey.location,
(CLLocationCoordinate2DIsValid(location.coordinate2D))
{
// Use the temp cached image for now while we get a new image
if let cachedImage = journey.cachedMap.image
{
setMapImage(cachedImage)
}
// Request an updated map image
journey.createMapImage {
[weak self] (image) in
DispatchQueue.main.async {
if let asyncCellBlock = self?.asyncCellBlock
{
let asyncResult = asyncCellBlock()
if let cell = asyncResult.cell,
let journey = asyncResult.journey
{
if (cell == self && journey.id == self?.journey?.id)
{
self?.setMapImage(image)
// Force the cell to redraw itself.
self?.setNeedsLayout()
}
}
}
}
}
}
else
{
setMapImage(nil)
}
}
I'm not sure if this is just a race condition with the UITableView updating several times in a small period of time.
I think this is because when the image is available then that index is not there. Since the table view cells are reusable, it loads the previous image since the current image is not loaded yet.
if let cachedImage = journey.cachedMap.image
{
setMapImage(cachedImage)
}
else {
// make imageView.image = nil
}
I can see you already cache the image but I think you should prepare the cell for reuse like this:
override func prepareForReuse() {
super.prepareForReuse()
let image = UIImage()
self.yourImageView.image = image
self.yourImageView.backgroundColor = .black
}
I'm new to Swift and generally lack of experience in programming. Currently I'm working on a project trying to display a list of star war characters on view controller, but I'm having some issues in passing data through networking Manager. When I ran the program, I couldn't get the name label displayed on the screen.
I have checked the tableView cell and the label is connected with viewController. I feel that the problem is somewhere related with networking manager but couldn't figure out by myself.
var charactersArray: [Characters] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
getCharacters(){
DispatchQueue.main.async {
self.starWarTableViewController.reloadData()
}
}
self.title = "Star War Characters"
// print(charactersArray), returns an empty array
}
private func getURL() -> String {
return "https://swapi.dev/api/people/"
}
func getCharacters(completion: #escaping () -> Void) {
self.starWarTableViewController.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
self.starWarTableViewController.dataSource = self
self.starWarTableViewController.delegate = self
DispatchQueue.global(qos: .background).async {
for i in 1...20 {
NetworkingManager.shared.getDecodedObject(from: self.getURL() + "\(i)"){
(characters: Characters?, error) in
guard let characters = characters else{ return }
self.charactersArray.append(characters)
print(self.charactersArray) //this will return an array list of characters names
}
}
print(self.charactersArray) // here the characterArray is empty
}
completion()
}
For what I found, the issue seems in my networking manager or the table view cell
enum NetworkError: Error {
case invalidURLString
}
final class NetworkingManager{
static let shared = NetworkingManager()
private init(){
}
func getDecodedObject <T: Decodable> (from urlString: String, completion: #escaping (T?, Error?) -> Void){
guard let url = URL(string: urlString) else {
completion(nil, NetworkError.invalidURLString)
return
}
URLSession.shared.dataTask(with: url){
(data, response, error) in
guard let data = data else{
completion(nil, error)
return }
guard let characters = try? JSONDecoder().decode(T.self, from: data) else{ return }
completion(characters, nil)
}.resume()
}
}
class TableViewCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
func configure (with characters: Characters) {
self.nameLabel.text = characters.name
}
}
Here is Characters
struct Characters: Decodable {
let name: String
enum CodingKeys: String, CodingKey {
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
// print(name), this will return a list of character names
}
}
This is the tableview extension
extension ViewController: UITableViewDataSource, UITableViewDelegate{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.charactersArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.configure(with: self.charactersArray[indexPath.row])
// cell.nameLabel.text = "Hi"
return cell
}
}
Right now, you are initiating a series of asynchronous network requests, but calling the completion handler after the requests are initiated, rather than waiting for when them to finish. Thus, when you call completion, the results have not been received.
When you start a bunch of asynchronous tasks and you want to know when they are done, a common pattern is to use DispatchGroup, calling enter before the asynchronous call, calling leave in the completion handler, and then specifying what you want to do when they are all done in a closure supplied to notify. If you put your call to your own completion handler in notify, then it will not get called until the requests are done and you have data to present in the UI.
A few other observations:
getDecodedObject is already asynchronous method, so there's no need to dispatch it to a background queue.
You will want to update both the model and the UI from the main queue. (URLSession calls its completion handlers on a serial background queue.) To accomplish this can basically tell the aforementioned notify to run its closure on the .main queue. I would also not update the model object until you are ready to update the UI.
When performing a series of asynchronous network requests in parallel, you have no assurances of the order in which they may complete. So, you would generally want to use some structure that is independent of the order that the tasks finish, such as a dictionary. Then, when they are done, if you want to build a sorted array of results, then build the array of results from that.
Thus:
func getCharacters(completion: #escaping ([Characters]) -> Void) {
// to keep track of when the 20 requests finish
let group = DispatchGroup()
// temporary structure to keep track of the results
var charactersDictionary: [Int: Characters] = [:]
// perform the requests
for i in 1...20 {
group.enter()
NetworkingManager.shared.getDecodedObject(from: getURL() + "\(i)") { (characters: Characters?, error) in
defer { group.leave() }
guard let characters = characters else { return }
charactersDictionary[i] = characters
}
}
// when the group tells us that all 20 are done, let's build array of the
// results and pass it back to the main queue via the completion handler parameter.
group.notify(queue: .main) {
let results = (1...20).compactMap { charactersDictionary[$0] }
print(results)
completion(results)
}
}
Note, by the way, I have pulled the table configuration code out of this routine (as you really don't want to intermingle network code with UI code):
func configureTableView() {
starWarTableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
starWarTableView.dataSource = self
starWarTableView.delegate = self
}
I've also renamed that outlet to be starWarTableView, because it is not a view controller, but a table view:
Anyway, you would then call it like so:
override func viewDidLoad() {
super.viewDidLoad()
configureTableView()
getCharacters { characters in
self.charactersArray = characters
self.starWarTableView.reloadData()
}
}
I have a collection view and want to load images and other data asynchronously from firebase and display them within the cell. However, my current approach displays wrong images to the text data (they simply don't fit) and also, the image in the one specific cell changes few times until it settles down (sometime wrong, sometimes correct).
My code
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let photoCell = collectionView.dequeueReusableCell(withReuseIdentifier: "mainViewCollectionCell", for: indexPath) as! MainViewCollectionViewCell
// issue when refreshing collection view after a new challenge has been created
if (hitsSource?.hit(atIndex: indexPath.row) == nil) {
return photoCell
}
let challengeObject = Challenge(json: (hitsSource?.hit(atIndex: indexPath.row))!)
let group = DispatchGroup()
group.enter()
// async call
self.checkIfChallengeIsBlocked(completionHandler: { (IsUserBlocked) in
if (IsUserBlocked) {
return
}
else {
group.leave()
}
}, challengeObject: challengeObject)
group.notify(queue: .main) {
photoCell.setChallengeLabel(title: challengeObject.title)
// async call
photoCell.fetchChallengeImageById(challengeObject: challengeObject)
photoCell.checkIfToAddOrRemovePlayIcon(challengeObject: challengeObject)
// async call
self.dataAccessService.fetchUserById(completionHandler: { (userObject) in
photoCell.setFullName(userObject: userObject)
photoCell.setStarNumber(challengeObject: challengeObject)
}, uid: challengeObject.organizerId)
// async all
self.dataAccessService.fetchAllParticipantsByChallengeId(completionHandler: { (participationArray) in
photoCell.setParticipationNumber(challengeObject: challengeObject, participationArray: participationArray)
}, challengeId: challengeObject.id)
// resize image to collection view cell
self.activityView.removeFromSuperview()
}
return photoCell
}
...
Just to show you my MainViewCollectionViewCell
class MainViewCollectionViewCell: UICollectionViewCell {
...
public func fetchChallengeImageById(challengeObject:Challenge) {
self.dataAccessService.fetchChallengeImageById(completion: { (challengeImage) in
self.challengeImageView.image = challengeImage
self.layoutSubviews()
}, challengeId: challengeObject.id)
}
and DataAccessService.swift
class DataAccessService {
...
// fetch main challenge image by using challenge id
public func fetchChallengeImageById(completion:#escaping(UIImage)->(), challengeId:String) {
//throws {
BASE_STORAGE_URL.child(challengeId).child(IMAGE_NAME).getData(maxSize: 1 * 2048 * 2048,
completion:({ data, error in
if error != nil {
print(error?.localizedDescription as Any)
let notFoundImage = UIImage()
completion(notFoundImage)
} else {
let image = UIImage(data: data!)!
completion(image)
}
}))
}
...
public func fetchUserById(completionHandler:#escaping(_ user: User)->(), uid:String?) { //
throws{
var userObject = User()
let _userId = UserUtil.validateUserId(userId: uid)
USER_COLLECTION?.whereField("uid", isEqualTo: _userId).getDocuments(completion: {
(querySnapshot, error) in
if error != nil {
self.error = error
print(error?.localizedDescription as Any)
} else {
for document in querySnapshot!.documents {
userObject = User(snapShot: document)
completionHandler(userObject)
}
}
})
}
Could anyone tell me what I need to change for being able to fit the text data to the correct image in the cell?
With asynchronous calls to fetch user data, the fact that cells are re-used introduces two issues:
When a cell is re-used, make sure that you do not show the values for the prior cell while your asynchronous request is in progress. Either have collectionView(_:cellForItemAt:) reset the values or, better, have the cell’s prepareForReuse make sure the controls are reset.
In the asynchronous request completion handler, check to see if the cell is still visible before updating it. You do this by calling collectionView.cellForItem(at:). If the resulting cell is nil, then the cell is not visible and there's nothing to update.
Thus:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let photoCell = collectionView.dequeueReusableCell(withReuseIdentifier: "mainViewCollectionCell", for: indexPath) as! MainViewCollectionViewCell
// make sure to initialize these so if the cell has been reused, you don't see the old values
photoCell.label.text = nil
photoCell.imageView.image = nil
// now in your asynchronous process completion handler, check to make sure the cell is still visible
someAsynchronousProcess(for: indexPath.row) {
guard let cell = collectionView.cellForItem(at: indexPath) else { return }
// update `cell`, not `photoCell` in here
}
return photoCell
}
Obviously, if one asynchronous completion handler initiates another asynchronous request, then you have to repeat this pattern.
I am loading my UITableView using an Arrayin swift. What I want to do is after table has loaded my array should be ampty (want to remove all object in the array then it loads another data set to load another table view)
What I want to do is adding several UItables dinamically to a UIScrollView and load all the data to every UITableView initially. Then user can scroll the scrollview horizontally and view other tables.So in my ViewDidLoadI am doing something like this.
for i in 0..<dm.TableData.count {
self.catID=self.dm.TableData[i]["term_id"] as? String
self.jsonParser()
}
then this is my jsonParser
func jsonParser() {
let urlPath = "http://www.liveat8.lk/mobileapp/news.php?"
let category_id=catID
let catParam="category_id"
let strCatID="\(catParam)=\(category_id)"
let strStartRec:String=String(startRec)
let startRecPAram="start_record_index"
let strStartRecFull="\(startRecPAram)=\(strStartRec)"
let strNumOfRecFull="no_of_records=10"
let fullURL = "\(urlPath)\(strCatID)&\(strStartRecFull)&\(strNumOfRecFull)"
print(fullURL)
guard let endpoint = NSURL(string: fullURL) else {
print("Error creating endpoint")
return
}
let request = NSMutableURLRequest(URL:endpoint)
NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) in
do {
guard let data = data else {
throw JSONError.NoData
}
guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? NSDictionary else {
throw JSONError.ConversionFailed
}
print(json)
if let countries_list = json["data"] as? NSArray
{
for (var i = 0; i < countries_list.count ; i++ )
{
if let country_obj = countries_list[i] as? NSDictionary
{
//self.TableData.append(country_obj)
self.commonData.append(country_obj)
}
}
//self.updateUI()
if self.commonData.isEmpty
{
}
else
{
self.updateUI()
}
}
} catch let error as JSONError {
print(error.rawValue)
} catch let error as NSError {
print(error.debugDescription)
}
}.resume()
}
Then UpdateUI()
func updateUI()
{
print("COMMON DATA ARRAY\(self.commonData)")
// print("NEWS DATA ARRAY\(self.newsNews)")
//print("SPORTS DATA ARRAY\(self.sportsNews)")
let tblY:CGFloat=segmentedControl.frame.origin.y+segmentedControl.frame.size.height
tblNews=UITableView.init(frame: CGRectMake(x,0 , self.screenWidth, self.screenHeight-tblY))
tblNews.tag=index
tblNews.delegate=self
tblNews.dataSource=self
tblNews.backgroundColor=UIColor.blueColor()
self.mainScroll.addSubview(tblNews)
x=x+self.screenWidth
index=index+1
tblNews.reloadData()
}
`UITableView` use this `commonData` array as the data source. Now when I scroll table view data load with previous data too.So what is the best way to do this? or else please tell me how can use `self.commonData.removeAll()` after 1 `UITableView` has loaded.Currently I did in `CellforrowAtIndex`
if indexPath.row == self.commonData.count-1
{
self.commonData.removeAll()
}
return cell
but this doesn't solve my problem
You should have separate sets of data, possibly arrays, for each UITableView. iOS will call back to your datasource delegate methods to request data.
It is important that you not delete data from the arrays because iOS is going to call your data source delegate methods expecting data. Even if you display the data in the table views initially, the user may scroll the scroll view causing one of the UITableView's to call your delegate methods to get the data again.
The data source delegate methods, such as func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell have a UITableView parameter that you can use to determine which data source is appropriate.
For example, you might have:
self.commonData1 = ["a", "b", "c"]
self.commonData2 = ["d", "e", "f"]
And you need to keep track of any tables you add to your scroll view:
self.tableView1 = ...the table view you create & add to scroll view
self.tableView2 = ...the table view you create & add to scroll view
And when you're responding to data source calls:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if tableView == self.tableView1 {
return 1
} else if tableView == self.tableView2 {
return 1
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == self.tableView1 {
return self.commonData1.count
} else if tableView == self.tableView2 {
return self.commonData2.count
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! IdeaTableViewCell
if tableView == self.tableView1 {
cell.textLabel?.text = self.commonData1[indexPath.row]
} else if tableView == self.tableView2 {
cell.textLabel?.text = self.commonData2[indexPath.row]
}
return cell
}
I'm having a rather common issue, but the solutions so far have not given me the desired result.
I have a UITableView that I am populating with information that I have parsed from a JSON pulled from the web at run time. The JSON retrieval starts in func viewDidLoad() using the NSURLSession.dataTaskWithURL(NSURL, completionHandler) function. The cells of the table are then populated within the completionHandler.
The cells used in the table are custom, and all UI elements in the cell have default values set through the interface builder.
When the table appears it is completely empty, though it does have the proper cell height. The reason I found for this behavior is that the reloadData function isn't being called at the right time due to multithreading/multiprocessing and the suggested solution is to have
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
somewhere in the code to allow the reload to occur correctly. However, this only partially works. The table flickers with what looks like content, and then immediately goes blank again until I scroll. Once I scroll the reloadData function works as expected, but only after I scroll.
I've tried having the reloadData function in various locations (such as in viewWillAppear) with no luck. Are there any ideas or troubleshooting tips that I can try?
Edit #1 - Request for completion handler
var listTask = session.dataTaskWithURL(listUrl, completionHandler: {(data, response, error) in
if error == nil {
var json:NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers, error: nil) as NSDictionary
var topGames = json["top"] as [NSDictionary]
var currGame:NSDictionary
var toAdd:Game
for var i=0; i < topGames.count; ++i {
currGame = topGames[i]["game"] as NSDictionary
toAdd = Game(
id: String(currGame["_id"] as CLong),
name: currGame["name"] as NSString,
boxArtImageUrl: (currGame["box"] as NSDictionary)["medium"] as NSString,
boxArtImage: nil,
isFetchingBoxArt: false,
totalViewers: topGames[i]["viewers"] as Int,
totalChannels: topGames[i]["channels"] as Int,
topChannel: "N/A")
self.games.append(toAdd)
}
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
}
}
Note that the self.games array is used in func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCel to fill in the information in the cells.
Edit #2 - Request for cellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:TopGameListCell = self.tableView.dequeueReusableCellWithIdentifier("topGameCell") as TopGameListCell
if games.count > 0 {
var game:Game = games[indexPath.row]
cell.nameLabel?.text = game.name
cell.totalViewersLabel.text = String(game.totalViewers)
cell.totalChannelsLabel.text = String(game.totalChannels)
cell.topChannelLabel.text = game.topChannel
if game.boxArtImage != nil {
cell.boxArtImageView.image = game.boxArtImage
} else {
if(!game.isFetchingBoxArt) {
cell.boxArtImageView.image = placeholderImage
gatherGameBoxArtImageForCell(game.boxArtImageUrl, indexPath: indexPath)
}
}
}
return cell;
}
I forgot about the extra fetch that I start within the gatherGameBoxArtImageForCell. Basically if I don't already have the image, download it. The if/else for the image seems to be causing the flicker to occur. If I let it sit long enough, it finally shows the table. The idea for me is that the placeholder image would show until the image is downloaded, and then reloadData is called after the image is downloaded. Here is the image fetch completionHandler:
var task = session.dataTaskWithURL(url, completionHandler: {(data, response, error) in
var game = self.games[indexPath.row]
if error == nil {
if(game.boxArtImage == nil) {
var cellForImage:TopGameListCell? = self.tableView.cellForRowAtIndexPath(indexPath) as? TopGameListCell
if cellForImage != nil {
self.games[indexPath.row].boxArtImage = UIImage(data: data)
cellForImage?.boxArtImageView.image = self.games[indexPath.row].boxArtImage
}
}
} else {
println(error)
}
game.isFetchingBoxArt = false
self.tableView.reloadData()
})
It appears that when I was trying to have the images load in the background it was hogging up the main thread. What I did was changed the following from
if(!game.isFetchingBoxArt) {
cell.boxArtImageView.image = placeholderImage
gatherGameBoxArtImageForCell(game.boxArtImageUrl, indexPath: indexPath)
}
to
if(!game.isFetchingBoxArt) {
cell.boxArtImageView.image = placeholderImage
dispatch_async(dispatch_get_main_queue(), {
self.gatherGameBoxArtImageForCell(game.boxArtImageUrl, indexPath: indexPath)
})
}
I decided to do this after thinking on how the normal solution is to place reloadData in a dispatch_async. Apparently the reasoning is the same for the issue I was having, so now the download of the images runs asynchronously.