Using Almofireimage to set image in a Custom Tableview Cell - ios

I'm using Alamofireimage to set an image based on a remote url on a UIImageView in my Custom UITableViewCell however the results are (1) the images aren't set until you scroll and (2) even though I'm using StackViews for my autolayout the sizes of the images displayed in the app vary wildly. Any idea what I'm doing wrong?
import UIKit
import Alamofire
import AlamofireImage
class AppsTableViewController: UITableViewController {
let session = URLSession.shared
var rekos: [Reko]? {
didSet {
DispatchQueue.main.async {
self.tableView?.reloadData()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
RekoManager().downloadAndConvertRekos(rekoType: .apps) { (result) in
print("Reko Count = \(result.count)")
self.rekos = result
}
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let count = rekos?.count else {return 0}
return count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AppCell", for: indexPath) as! AppsTableViewCell
if let reko = rekos?[indexPath.row] {
cell.titleLabel.text = reko.title
cell.descriptionLabel.text = reko.description
if let imageUrlString = reko.imageUrlString {
if let imageURL = URL(string: imageUrlString) {
//TODO IMPLEMENT SPINNER OVER IMAGE
cell.appImageView.af_setImage(withURL: imageURL, placeholderImage: UIImage(named: "placeholder"), filter: nil, progress: nil, progressQueue: DispatchQueue.main, imageTransition: .noTransition, runImageTransitionIfCached: false, completion: { (result) in
cell.setNeedsLayout()
cell.layoutIfNeeded()
})
}
}
}
return cell
}
}

Related

Swift: How to Load JSON in different Table View

Hi everybody I'm new to Swift and I need help.
So I have three different JSON that will show in different moments, The first JSON has been loading perfectly, but when I clicking on the item to reload another JSON and show the detail nothing appears.
I'm confused about details:
Need I have three differents table Views for each JSON? or the only one is enough?
When I working with data (JSON) need I use a specific function to prepare the new JSON that will appear as "prepare"?
In my project I have:
Two view controllers: ViewController(default) and DetailViewController.
In my Main.Storyboard: Tab Bar Controller --> Navigation --> Table View
The code of the first View controller:
import UIKit
class ViewController: UITableViewController {
var categories = [Category]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let urlString = "https://www.themealdb.com/api/json/v1/1/categories.php"
if let url = URL(string: urlString) {
if let data = try? Data(contentsOf: url) {
parse(json: data)
} else {
print("error connecting")
}
}
}
func parse(json: Data) {
let decoder = JSONDecoder()
print("parse called")
do {
let jsonCategories = try decoder.decode(Categories.self, from: json)
categories = jsonCategories.categories
tableView.reloadData()
} catch {
print("error parsin: \(error)")
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categories.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let category = categories[indexPath.row]
cell.textLabel?.text = category.idCategory
cell.detailTextLabel?.text = category.strCategoryDescription
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let newViewController = DetailViewController()
self.navigationController?.pushViewController(newViewController, animated: true)
}
}
The code of the second view controller:
import UIKit
import WebKit
class DetailViewController: UIViewController {
var webView: WKWebView!
var meals = [Meal]()
override func loadView() {
webView = WKWebView()
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let urlString = "https://www.themealdb.com/api/json/v1/1/filter.php?c=Beef"
if let url = URL(string: urlString) {
if let data = try? Data(contentsOf: url) {
parse(json: data)
} else {
print("error connecting")
}
}
}
func parse(json: Data) {
let decoder = JSONDecoder()
print("parse called")
do {
let jsonMeals = try decoder.decode(Meals.self, from: json)
meals = jsonMeals.meals
print(String(format:"read %d meals", meals.count))
} catch {
print("error parsin: \(error)")
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return meals.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let meal = meals[indexPath.row]
cell.textLabel?.text = meal.idMeal
cell.detailTextLabel?.text = meal.strMeal
return cell
}
}
The function parse(json: Data) on DetailViewController does not call tableView.reloadData() so the new data cannot be loaded to UITableView
Need I have three differents table Views for each JSON? or the only one is enough?
If you're loading different JSON files but want to show in the same view. You only need 1 UITableViewCell.
When you load & decode JSON, consider moving it to background queue to avoid blocking main thread.

I am using iOS' prefetching to download images for tableview, but why is the display still jerky?

I have an app which fetches RSS feeds and then uses image URLs from those feeds to download and populate an ImageView. I had a go at implementing iOS prefetching (see code below), but it's still glitchy, like it pauses briefly when scrolling (even when scrolling at a 'reasonable speed). I'm not sure what the best way is to debug this - any advice?
This is the TableViewController's cellForRowAt:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "NewsTableViewCell", for: indexPath) as! NewsTableViewCell
if let item = rssItems?[indexPath.item] {
cell.item = item
cell.selectionStyle = .none
cell.descriptionText.text = item.isExpanded ? item.description : ""
cell.descriptionText.isHidden = item.isExpanded ? false : true
cell.delegate = self
if let imageView = cell.viewWithTag(100) as? UIImageView {
if (rssItems![indexPath.row].imageData != nil) {
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(data: item.getImageData()!)
} else {
imageView.image = nil
if cell.item.imageLink == "" {
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named:item.logoImage!)
} else {
if let imageUrl:URL = URL(string: cell.item.imageLink!) {
cell.item.imageData = try? Data(contentsOf: imageUrl)
imageView.image = UIImage(data: cell.item.imageData!)
}
}
}
}
}
return cell
}
Here is the prefetching extension (I didn't implement the cancel download part):
// MARK: - UITableViewDataSourcePrefetching
extension NewsTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
// print("prefetchRowsAt \(indexPaths)")
indexPaths.forEach { self.rssItems![$0.row].setImageData() }
}
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
// print("cancelPrefetchingForRowsAt \(indexPaths)")
// indexPaths.forEach { self.cancelDownloadingImage(forItemAtIndex: $0.row) }
}
}
Then the setImageData function comes from a class which makes up an object representing a feed item along with the URL to the image for the given cell:
func setImageData() {
if self.imageLink != nil {
guard let imageUrl:URL = URL(string: self.imageLink!) else {
return
}
guard let imageData = try? Data(contentsOf: imageUrl) else {
return
}
self.imageData = imageData
}
}

Is this the proper way to use heightForRowAt?

Im trying to implement dynamically sized row heights based on the size of downloaded images. The problem I am encountering is that the images are not downloaded when the function heightForRowAt is run. What is the proper way to implement this code. images is an array of UIImage, rowHeights is an array of type CGFloat and imageURLS is a string array of imageURLS.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Reuse", for: indexPath) as! TableViewCell
// Configure the cell...
///////////////////////
if(cell.cellImageView.image == nil){
let downloadURL = URL(string: self.imageURLS[indexPath.row])
URLSession.shared.dataTask(with: downloadURL!) { (data, _, _) in
if let data = data {
let image = UIImage(data: data)
DispatchQueue.main.async {
cell.cellImageView.image = image
cell.cellImageView.contentMode = .scaleAspectFit
self.images.insert(image!, at: 0)
let aspectRatio = Float((cell.cellImageView?.image?.size.width)!/(cell.cellImageView?.image?.size.height)!)
print("aspectRatio: \(aspectRatio)")
tableView.rowHeight = CGFloat(Float(UIScreen.main.bounds.width)/aspectRatio)
print("tableView.rowHeight: \(tableView.rowHeight)")
self.rowHeights.insert(CGFloat(Float(UIScreen.main.bounds.width)/aspectRatio), at: 0)
tableView.reloadRows(at: [indexPath], with: .top)
}
}
}.resume()
}
///////////////////////
return cell
}
//What is the proper way to implement this function
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
print("Im in height for row")
return CGFloat(0.0)
}
If your asynchronous request may change the height of the cell, you should not update the cell directly, but rather you should completely reload the cell.
So, heightForRowAt and cellForRowAt will be called once for each visible cell before the image is retrieved. Since the image hasn't been retrieved yet, heightForRowAt will have to return some fixed value appropriate for a cell with no image. And cellForRowAt should detect that the image has not been retrieved and initiate that process. But when the image retrieval is done, rather than updating the cell directly, cellForRowAt should call reloadRows(at:with:). That will start the process again for this row, including triggering heightForRowAt to be called again, too. But this time, the image should be there, so heightForRowAt can now return an appropriate height and cellForRowAt can now just update the image view with no further network request.
For example:
class ViewController: UITableViewController {
private var objects: [CustomObject]!
override func viewDidLoad() {
super.viewDidLoad()
objects = [
CustomObject(imageURL: URL(string: "https://upload.wikimedia.org/wikipedia/commons/e/e8/Second_Life_Landscape_01.jpg")!),
CustomObject(imageURL: URL(string: "https://upload.wikimedia.org/wikipedia/commons/7/78/Brorfelde_landscape_2.jpg")!)
]
}
let imageCache = ImageCache()
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
let imageURL = objects[indexPath.row].imageURL
if let image = imageCache[imageURL] {
// if we got here, we found image in our cache, so we can just
// update image view and we're done
cell.customImageView.image = image
} else {
// if we got here, we have not yet downloaded the image, so let's
// request the image and then reload the cell
cell.customImageView.image = nil // make sure to reset the image view
URLSession.shared.dataTask(with: imageURL) { data, _, error in
guard let data = data, error == nil else {
print(error ?? "Unknown error")
return
}
if let image = UIImage(data: data) {
self.imageCache[imageURL] = image
DispatchQueue.main.async {
// NB: This assumes that rows cannot be inserted while this asynchronous
// request is underway. If that is not a valid assumption, you will need to
// go back to your model and determine what `IndexPath` now represents
// this row in the table.
tableView.reloadRows(at: [indexPath], with: .middle)
}
}
}.resume()
}
return cell
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let imageURL = objects[indexPath.row].imageURL
if let image = imageCache[imageURL] {
let size = image.size
return view.bounds.size.width * size.height / size.width
} else {
return 0
}
}
}
Where a simple image cache (which is not relevant to your question, but I include for the sake of completeness) is as follows:
class ImageCache {
private let cache = NSCache<NSURL, UIImage>()
private var observer: NSObjectProtocol!
init () {
observer = NotificationCenter.default.addObserver(forName: .UIApplicationDidReceiveMemoryWarning, object: nil, queue: nil) { [weak self] _ in
self?.cache.removeAllObjects()
}
}
deinit {
NotificationCenter.default.removeObserver(observer)
}
subscript(key: URL) -> UIImage? {
get {
return cache.object(forKey: key as NSURL)
}
set (newValue) {
if let image = newValue {
cache.setObject(image, forKey: key as NSURL)
} else {
cache.removeObject(forKey: key as NSURL)
}
}
}
}

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()
}
}
}

Using UIImageView+AFNetworking, images doesn't appear until scrolling

I'm using UIImageView+AFNetworking to load images in table cell, but
images downloaded from server aren't shown in cell until scroll down and up again. I thought that I needed to reload data in tableView, but I don't know where to put said reload code.
Here's the code:
#IBOutlet weak var refresherTool: UIRefreshControl!
override func viewDidLoad() {
super.viewDidLoad()
let _ = dataRefresher.sharedInstance.refreshVideos(fromUrl: "https://api.vid.me/videos/featured", withLimit: "100", withOffset: "0", completion: {
featuredVideos in dataRefresher.sharedInstance.featuretVideos = featuredVideos
DispatchQueue.main.async {
self.tableView.reloadData()
}
})
}
#IBAction func refreshBtn(_ sender: UIRefreshControl) {
let _ = dataRefresher.sharedInstance.refreshVideos(fromUrl: "https://api.vid.me/videos/featured", withLimit: "100", withOffset: "0", completion: {
featuredVideos in dataRefresher.sharedInstance.featuretVideos = featuredVideos
self.refresherTool.endRefreshing()
DispatchQueue.main.async{
self.tableView.reloadData()
}
})
refresherTool.endRefreshing()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if dataRefresher.sharedInstance.featuretVideos == nil {
return 0
}
return 100
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "featuredCell", for: indexPath)
// if dataRefresher.sharedInstance.featuretVideos == nil { return cell}
let newImage = UIImageView()
newImage.setImageWithUrl(URL.init(string: dataRefresher.sharedInstance.getVideosThumbnailUrls(fromVideos: dataRefresher.sharedInstance.featuretVideos)[indexPath.row])!, placeHolderImage: #imageLiteral(resourceName: "first"))
// newImage.setImageWithUrl(URL.init(string: urls[indexPath.row])!)
cell.imageView?.image = newImage.image
cell.textLabel?.text = dataRefresher.sharedInstance.getVideosTitels(fromVideos: dataRefresher.sharedInstance.featuretVideos)[indexPath.row]
print(indexPath.row)
print(newImage.image!)
return cell
}
The setImageWithUrl method is asynchronous, meaning that if the referenced image is not in the local cache yet, it will only be downloaded some time in the future.
However, your code tries to get the image out right away:
let newImage = UIImageView()
newImage.setImageWithUrl(..., placeHolderImage: ...)
cell.imageView?.image = newImage.image <-- synch call, image is nil here at first!
Fortunately, there's no need to reload the table view or do any other arcane tricks; just stop doing unnecessary things.
Remove the temporary UIImageView and load the image into the cell's UIImageView directly.
The above code becomes:
cell.imageView?.setImageWithUrl(..., placeHolderImage: ...)

Resources