Swift: Display image from UIImage array in table view - ios

I am getting images from an async request and adding them to a [UIImage]() so that I can populate my UITableView images with those from the array. The problem is, I keep getting Fatal error: Array index out of range in the cellForRowAtIndexPath function when this is called, and I suspect it may be because I'm making an async call? Why can't I add an image from the array to the table view row?
var recommendedImages = [UIImage]()
var jsonLoaded:Bool = false {
didSet {
if jsonLoaded {
// Reload tableView on main thread
dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)) { // 1
dispatch_async(dispatch_get_main_queue()) { // 2
self.tableView.reloadData() // 3
}
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// ...
let imageURL = NSURL(string: "\(thumbnail)")
let imageURLRequest = NSURLRequest(URL: imageURL!)
NSURLConnection.sendAsynchronousRequest(imageURLRequest, queue: NSOperationQueue.mainQueue(), completionHandler: { response, data, error in
if error != nil {
println("There was an error")
} else {
let image = UIImage(data: data)
self.recommendedImages.append(image!)
self.jsonLoaded = true
}
})
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var songCell = tableView.dequeueReusableCellWithIdentifier("songCell", forIndexPath: indexPath) as! RecommendationCell
songCell.recommendationThumbnail.image = recommendedImages[indexPath.row]
return songCell
}
Edit: My numberOfRowsInSection method. recommendedTitles is from the same block of code that I excluded. It's always going to be 6.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return recommendedTitles.count
}

Your error is you return 6 in numberOfRowsInSection,so tableview know that you have 6 cell
But,when execute cellForRowAtIndexPath,your image array is empty,so it crashed.
Try this
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return recommendedImages.count
}
Also switch to main queue,this is enough
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})

Related

Limit the amount of cells shown in tableView, load more cells when scroll to last cell

I'm trying to set up a table view that only shows a specific amount of cells. Once that cell has been shown, the user can keep scrolling to show more cells. As of right now I'm retrieving all the JSON data to be shown in viewDidLoad and storing them in an array. Just for example purposes I'm trying to only show 2 cells at first, one the user scrolls to bottom of screen the next cell will appear. This is my code so far:
class DrinkViewController: UIViewController {
#IBOutlet weak var drinkTableView: UITableView!
private let networkManager = NetworkManager.sharedManager
fileprivate var totalDrinksArray: [CocktailModel] = []
fileprivate var drinkImage: UIImage?
fileprivate let DRINK_CELL_REUSE_IDENTIFIER = "drinkCell"
fileprivate let DRINK_SEGUE = "detailDrinkSegue"
var drinksPerPage = 2
var loadingData = false
override func viewDidLoad() {
super.viewDidLoad()
drinkTableView.delegate = self
drinkTableView.dataSource = self
networkManager.getJSONData(function: urlFunction.search, catagory: urlCatagory.cocktail, listCatagory: nil, drinkType: "margarita", isList: false, completion: { data in
self.parseJSONData(data)
})
}
}
extension DrinkViewController {
//MARK: JSON parser
fileprivate func parseJSONData(_ jsonData: Data?){
if let data = jsonData {
do {
let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? [String : AnyObject]//Parses data into a dictionary
// print(jsonDictionary!)
if let drinkDictionary = jsonDictionary!["drinks"] as? [[String: Any]] {
for drink in drinkDictionary {
let drinkName = drink["strDrink"] as? String ?? ""
let catagory = drink["strCategory"] as? String
let drinkTypeIBA = drink["strIBA"] as? String
let alcoholicType = drink["strAlcoholic"] as? String
let glassType = drink["strGlass"] as? String
let drinkInstructions = drink["strInstructions"] as? String
let drinkThumbnailUrl = drink["strDrinkThumb"] as? String
let cocktailDrink = CocktailModel(drinkName: drinkName, catagory: catagory, drinkTypeIBA: drinkTypeIBA, alcoholicType: alcoholicType, glassType: glassType, drinkInstructions: drinkInstructions, drinkThumbnailUrl: drinkThumbnailUrl)
self.totalDrinksArray.append(cocktailDrink)
}
}
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
}
DispatchQueue.main.async {
self.drinkTableView.reloadData()
}
}
//MARK: Image Downloader
func updateImage (imageUrl: String, onSucceed: #escaping () -> Void, onFailure: #escaping (_ error:NSError)-> Void){
//named imageData because this is the data to be used to get image, can be named anything
networkManager.downloadImage(imageUrl: imageUrl, onSucceed: { (imageData) in
if let image = UIImage(data: imageData) {
self.drinkImage = image
}
onSucceed()//must call completion handler
}) { (error) in
onFailure(error)
}
}
}
//MARK: Tableview Delegates
extension DrinkViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//return numberOfRows
return drinksPerPage
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = drinkTableView.dequeueReusableCell(withIdentifier: DRINK_CELL_REUSE_IDENTIFIER) as! DrinkCell
//get image from separate url
if let image = totalDrinksArray[indexPath.row].drinkThumbnailUrl{//index out of range error here
updateImage(imageUrl: image, onSucceed: {
if let currentImage = self.drinkImage{
DispatchQueue.main.async {
cell.drinkImage.image = currentImage
}
}
}, onFailure: { (error) in
print(error)
})
}
cell.drinkLabel.text = totalDrinksArray[indexPath.row].drinkName
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let image = totalDrinksArray[indexPath.row].drinkThumbnailUrl{
updateImage(imageUrl: image, onSucceed: {
}, onFailure: { (error) in
print(error)
})
}
performSegue(withIdentifier: DRINK_SEGUE, sender: indexPath.row)
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastElement = drinksPerPage
if indexPath.row == lastElement {
self.drinkTableView.reloadData()
}
}
}
I saw this post: tableview-loading-more-cell-when-scroll-to-bottom and implemented the willDisplay function but am getting an "index out of range" error.
Can you tell me why you are doing this if you are getting all results at once then you don't have to limit your display since it is automatically managed by tableview. In tableview all the cells are reused so there will be no memory problem. UITableViewCell will be created when it will be shown.
So no need to limit the cell count.
I dont now what you are doing in your code but:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastElement = drinksPerPage // no need to write this line
if indexPath.row == lastElement { // if block will never be executed since indexPath.row is never equal to drinksPerPage.
// As indexPath starts from zero, So its value will never be 2.
self.drinkTableView.reloadData()
}
}
Your app may be crashing because may be you are getting only one item from server.
If you seriously want to load more then you can try this code:
Declare numberOfItem which should be equal to drinksPerPage
var numberOfItem = drinksPerPage
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//return numberOfRows
return numberOfItem
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == numberOfItem - 1 {
if self.totalDrinksArray.count > numberOfItem {
let result = self.totalDrinksArray.count - numberOfItem
if result > drinksPerPage {
numberOfItem = numberOfItem + drinksPerPage
}
else {
numberOfItem = result
}
self.drinkTableView.reloadData()
}
}
}

Index out of range when trying to read an array of parsed JSON

I have a function that reads the JSON from the NYTimes API - I'm trying to populate a table view with the headlines. This is the function:
func getJSON() {
let url = NSURL(string: nyTimesURL)
let request = NSURLRequest(url: url as! URL)
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: request as URLRequest) { (data, response, error) in
if error != nil {
print(error)
}
let json = JSON(data: data!)
let results = json["results"].arrayValue
for title in results {
let titles = title["title"].stringValue
print(titles)
let count: Int = title.count
self.numberOfStories = count
self.headlines.append(titles)
self.tableView.reloadData()
print("\n\n\nHeadlines array: \(self.headlines)\n\n\n")
}
}
task.resume()
}
And then as class variables I have
var headlines = [String]()
var numberOfStories = 1
If I hardcode the cell.textLabel, I can run the app and see the headlines array is properly populated with all the headlines. But if I try to set the cell label to self.headlines[indexPath.row], I get an index out of range crash. I've tried putting the tableView.reloadData() call in the main thread (DispatchQueue.main.async{}) but that's not the issue.
How can I get the headlines to properly display?
Thanks for any help!
EDIT: Tableview methods:
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.numberOfStories
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "JSONcell", for: indexPath) as! JSONTableViewCell
cell.cellLabel.text = self.headlines[indexPath.row]
return cell
}
You need to get rid of your numberOfStories property. Use headlines.count instead.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return headlines.count
}
Your cellForRowAt and numberOfRowsInSection must be based on the same data.
And be sure you call reloadData inside DispatchQueue.main.async since the data task completion block is being called from a background queue.

swift: Completion handler

So, I have method loadData() which download datas from parse.com
And I should present all images show in table view.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ReusableCell", forIndexPath: indexPath) as! LeaguesTableViewCell
loadData { (success) in
if success {
cell.leagueImage.image = UIImage(data: self.leaguesImage[indexPath.row])
cell.leagueNameLabel.text = self.leagues[indexPath.row]["name"] as? String
} else {
cell.leagueNameLabel.text = "Wait"
}
}
return cell
}
Its didn't work. I call my function in viewDidLoad() but its not correct too, table view is empty.
Cuz my array is empty
My
The basic procedure for loading data into a UITableView is:
Load the data
Reload the table view
Return the number of sections in numberOfSectionsInTableView: method: In your case there is only 1 section.
Return the number of rows in tableView:numberOfRowsInSection:: In your case return the number of leagues if the data is loaded. If the data is not loaded then return 1 so that the table view has at least one row to display the "Wait" message.
Create and populate the cells from the data: Use leagues and leaguesImage.
Example:
private var loaded = false
override func viewDidLoad() {
super.viewDidLoad()
loaded = false
loadData() { success in
NSOperationQueue.mainQueue().addOperationWithBlock() {
self.loaded = success
self.tableView.reloadData()
}
}
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection: Int) -> Int {
if loaded {
return leagues.count
}
else {
return 1
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ReusableCell", forIndexPath: indexPath) as! LeaguesTableViewCell
if loaded {
cell.leagueImage.image = UIImage(data: self.leaguesImage[indexPath.row])
cell.leagueNameLabel.text = self.leagues[indexPath.row]["name"] as? String
}
else {
cell.leagueNameLabel.text = "Wait"
}
return cell
}
Try to set delegate and datasource first. If you have separate datasource other than view controller, retain it otherwise you will not get any callback.

UITableViewAutomaticDimension Cells resize after scroll or pull to refresh

I have added a table view, and I am display image in the cells. I have also added this code:
tableView.estimatedRowHeight = 175
tableView.rowHeight = UITableViewAutomaticDimension
So that the cells resize depending on the image.
When I launch my app though, I get this :
And the images do not load untill I start scrolling...If I scroll down half the page then go back to the top, I get this(which is correct):
Also if I remove the like button and just has the image alone in a cell, when I launch my app if I wait 3 seconds without touching anything the cells resize on they're own..?!
Any ideas? I have researched on google and tried the odd solution for the older versions of Xcode, But nothing seems to work!
Here is the rest of my code from the TableViewController:
extension TimelineViewController: UITableViewDataSource {
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 46
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return timelineComponent.content.count
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCellWithIdentifier("PostHeader") as! PostHeaderTableViewCell
let post = self.timelineComponent.content[section]
headerCell.post = post
return headerCell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PostCell") as! PostTableViewCell
//cell.postImageView?.image = UIImage(named: "Background.png")
let post = timelineComponent.content[indexPath.section]
post.downloadImage()
post.fetchLikes()
cell.post = post
cell.layoutIfNeeded()
return cell
}
}
extension TimelineViewController: UITableViewDelegate {
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
timelineComponent.targetWillDisplayEntry(indexPath.section)
}
Download image code:
func downloadImage() {
// 1
image.value = Post.imageCache[self.imageFile!.name]
if image is not downloaded yet, get it
if (image.value == nil) {
imageFile?.getDataInBackgroundWithBlock { (data: NSData?, error: NSError?) -> Void in
if let data = data {
let image = UIImage(data: data, scale: 2.0)!
self.image.value = image
// 2
Post.imageCache[self.imageFile!.name] = image
}
}
}
}
// MARK: PFSubclassing
extension Post: PFSubclassing {
static func parseClassName() -> String {
return "Post"
}
override class func initialize() {
var onceToken : dispatch_once_t = 0;
dispatch_once(&onceToken) {
// inform Parse about this subclass
self.registerSubclass()
// 1
Post.imageCache = NSCacheSwift<String, UIImage>()
}
}
}
And here is my TableViewCell:
var post: Post? {
didSet {
postDisposable?.dispose()
likeDisposable?.dispose()
if let oldValue = oldValue where oldValue != post {
oldValue.image.value = nil
}
if let post = post {
postDisposable = post.image
.bindTo(postImageView.bnd_image)
likeDisposable = post.likes
.observe { (value: [PFUser]?) -> () in
if let value = value {
//self.likesLabel.text = self.stringFromUserList(value)
self.likeButton.selected = value.contains(PFUser.currentUser()!)
// self.likesIconImageView.hidden = (value.count == 0)
} else {
//self.likesLabel.text = ""
self.likeButton.selected = false
//self.likesIconImageView.hidden = true
}
}
}
}
}
Any help is really appreciated!
I guess, you need to reload the cell when the image is finally loaded, because tableView needs to recalculate cell height (and the whole contentHeight) when image with new size arrives
post.downloadImage { _ in
if tableView.indexPathForCell(cell) == indexPath {
tableView.reloadRowsAtIndexPaths([indexPath], animation: .None)
}
}
and downloadImage method needs to call completion closure. Something like that.
func downloadImage(completion: ((UIImage?) -> Void)?) {
if let imageValue = Post.imageCache[self.imageFile!.name] {
image.value = imageValue
completion?(imageValue)
return
}
//if image is not downloaded yet, get it
imageFile?.getDataInBackgroundWithBlock { (data: NSData?, error: NSError?) -> Void in
if let data = data {
let image = UIImage(data: data, scale: 2.0)!
self.image.value = image
// 2
Post.imageCache[self.imageFile!.name] = image
completion?(image)
} else {
completion?(nil)
}
}
}

Creating TableViews in Swift with an Array

I'm attempting to use the result of one Rest call as an input for my TableView.
I've got an array named GamesList[String] that is synthesized in the viewDidLoad() function. This is the viewDidLoad() fuction:
override func viewDidLoad() {
super.viewDidLoad()
getState() { (json, error) -> Void in
if let er = error {
println("\(er)")
} else {
var json = JSON(json!);
print(json);
let count: Int = json["games"].array!.count
println("found \(count) challenges")
for index in 0...count-1{
println(index);
self.GamesList.append(json["games"][index]["game_guid"].string!);
}
}
}
}
The problem is that the functions for filling the TableView get executed before my GamesList array is filled up. These are the functions that fill the TableView:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return GamesList.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Game", forIndexPath: indexPath) as! UITableViewCell
cell.textLabel?.text = GamesList[indexPath.row]
cell.detailTextLabel?.text = GamesList[indexPath.row]
return cell
}
How do I force the tables to get filled up (refreshed) after my array has been filled?
use self.tableView.reloadData() after you append your values
getState() { (json, error) -> Void in
if let er = error {
println("\(er)")
} else {
var json = JSON(json!);
print(json);
let count: Int = json["games"].array!.count
println("found \(count) challenges")
for index in 0...count-1{
println(index);
self.GamesList.append(json["games"][index]["game_guid"].string!);
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}

Resources