I want to populate a table view by parsing JSON data received thanks to several Alamofire requests. However, the thing is that each cell has a different request from the others. Each cell makes a request based on IndexPath. Since the calls are asynchronous, not all requests are achieved...
My code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
var name = ""
let url = "https://........"+items[indexPath.section][indexPath.row]+"......"
Alamofire.request(url).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let swiftyJsonVar = JSON(responseData.result.value!)
let object = swiftyJsonVar["results"][0]
print(object)
name = object["name"].stringValue
cell.textLabel?.text = name
}
}
return cell
}
How would it be possible to achieve every request? Thanks in advance!
You should keep track of hooking indexpath with every request so you know what the value returned is for
struct Item
{
var indexPath:NSIndexPath!
func getData(index:NSIndexPath)
{
let url = "https://........"+items[indexPath.section][indexPath.row]+"......"
Alamofire.request(url).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let swiftyJsonVar = JSON(responseData.result.value!)
let object = swiftyJsonVar["results"][0]
globalArr// store indexpath and value
/// remove indexoath from allDownloads
/// send refresh to table
}
}
}
}
in cell for row
if(indexPath in globalArr)
{
// show it
}
else
{
// check whether it's download is in progress to avoid multiple same requests when scroll because of dequeuing
if(allDownloads doesn't contain inexpath)
{
// create object of item and start download
// allDownloads.add(object)
}
}
globalArr: keep track of all downloaded objects
allDownloads : keep track of in progress downloads
Related
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 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.
I have a tableview which acts as a newsfeed. The cells are filled from an array of newsfeed items. I get the JSON from the Server, create newsfeed items from that input and attach them to my newsfeed array. a newsfeed item contains a title, a description and an imageurl string.
At:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "ImageFeedItemTableViewCell1", for: indexPath) as! ImageFeedItemTableViewCell
var item = self.feed!.items[indexPath.row]
if (item.messageType == 1){
cell = tableView.dequeueReusableCell(withIdentifier: "ImageFeedItemTableViewCell1", for: indexPath) as! ImageFeedItemTableViewCell
cell.title.text = item.title
cell.description.text = item.contentText
if (item.imageURL as URL == URL(string: "noPicture")!)
{
cell.picture.image = UIImage(named:"empty")
}
else{
if (item.cachedImage == UIImage(named:"default-placeholder")){
let request = URLRequest(url: item.imageURL as URL)
cell.picture.image = item.cachedImage
cell.dataTask = self.urlSession.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
OperationQueue.main.addOperation({ () -> Void in
if error == nil && data != nil {
let image = UIImage(data: data!)
if (image != nil){
self.feed!.items[indexPath.row].cachedImage = image!
}
cell.picture.image = image
}
})
})
cell.dataTask?.resume()
}else
{
cell.picture.image = item.cachedImage
}
}
}
the cells from the rows get filled with my newsfeeditem data.
But since i keep all my newsfeeditems inside an array, the memory usage is high and gehts higher for each additional newsfeeditem. I want it to work with endless scrolling like twitter, so i wonder how experienced developers tackle this memory issue.
Your problem is in this lines or wherever you try to hold UIImage inside your array, this is really not advised and will cause crash due to memory since image is very large data and not advised to persist it in your RAM with UIImage inside array:
self.feed!.items[indexPath.row].cachedImage = image!
What you need to do is basically after fetch your image from URL, you save it to your app's documents folder and save the name or it's path that can distinct your image in cachedImage (just change the type to string or sth) and refetch it from your app's document folder when you need to show it in cellForRow
Flow: Fetch image -> save to disk and persist path in array -> refetch from disk with the path in cellForRow
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 currently in the process of creating an app to display the latest football scores. I've connected to an API through a URL and pulled back the team names for the english premier league into an array of strings.
The problem seems to come from populating the iOS table view that I intend to display the list of teams with. The data appears to be pulled from the API fine, but for some reason the TableView method which creates a cell and returns it doesn't seem to be called. The only time I can get the method to be called is when I actually hard code a value into the array of team names.
Here is my code:
class Main: UIViewController {
var names = [String]()
override func viewDidLoad() {
super.viewDidLoad()
let URL_String = "https://football-api.com/api/?Action=standings&APIKey=[API_KEY_REMOVED]&comp_id=1204"
let url = NSURL(string: URL_String)
let urlRequest = NSURLRequest(URL: url!)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let teams = json["teams"] as? [[String : AnyObject]] {
for team in teams {
if let name = team["stand_team_name"] as? String {
self.names.append(name)
}
}
}
} catch {
print("error serializing JSON: \(error)")
}
})
task.resume()
}
// Number of Sections In Table
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
// Number of Rows in each Section
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
// Sets the content of each cell
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.textLabel?.text = names[indexPath.row]
return cell
}
}
Just wondering if anyone can point me in the right direction here. This code doesn't crash or throw any errors, it just refuses to load a table view. The only reason I can possibly think of is that the array of team names is empty after completing a request to the API. However I've set breakpoints throughout and checked the values of local variables and the desired information is being pulled from the API as intended...
you are in the correct way , just refresh the table using reloadData once you got the new data from API
if let teams = json["teams"] as? [[String : AnyObject]] {
for team in teams {
if let name = team["stand_team_name"] as? String {
self.names.append(name)
}
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.yourtableViewname.reloadData()
})
}