images can't be display in table view - swift

I created a tableView with custom cells that each cell has an image.
In the model class, I created a func mainPulatesData() to use URLSession dataTask method to retrieve data from url, and convert data into UIImage in the completion handler block, then add image into an variable of array of UIImage.
The process of retrieve data and adding them into UIImage array was perform in DispatchQueue.global(qos: .userInitiated).async block. based on the print message, the images did be added into array.
however, even I created an instance of model class in tableView controller, and invokes the mainPulatesData() in viewDidlLoad, the image didn't show up in the table.
Based on other print message in table view controller class, I found even it can be added into array in model class, but it seems like doesn't work on the instance of model class in tableView controller.
that's the code in model class to gain image data:
func mainPulatesData() {
let session = URLSession.shared
if myURLs.count > 0{
print("\(myURLs.count) urls")
for url in myURLs{
let task = session.dataTask(with: url, completionHandler: { (data,response, error) in
let imageData = data
DispatchQueue.global(qos: .userInitiated).async {
if imageData != nil{
if let image = UIImage(data: imageData!){
self.imageList.append(image)
print("\(self.imageList.count) images added.")
}
}
else{
print("nil")
}
}
})
task.resume()
}
}
}
that's the code in view controller to create instance of model:
override func viewDidLoad() {
super.viewDidLoad()
myModel.mainPulatesURLs()
myModel.mainPulatesData()
loadImages()
}
private func loadImages(){
if myModel.imageList.count > 0{
tableView.reloadData()
}
else{
print("data nil")
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myModel.imageList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath) as! ImageTableViewCell
if myModel.imageList.count > 0{
let image = myModel.imageList[indexPath.row]
cell.tableImage = image
return cell
}
return cell
}

The reason is that the images or imageList isn't ready by the time cellForRowAt is called after you reloadData().
A good practice is to use placeholder images in the beginning and only load image when a table view cell is visible instead of everything at once. Something like:
// VC class
private var modelList = [MyModel(url: url1), MyModel(url: url2), ...]
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return modelList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath) as! ImageTableViewCell
cell.update(model: modelList[indexPath.row])
return cell
}
// Cell class
#IBOutlet weak var cellImageView: UIImageView!
func update(model: MyModel) {
model.fetchImage(callback: { image in
self.cellImageView.image = image
})
}
// Model class
final class MyModel: NSObject {
let url: URL
private var _imageCache: UIImage?
init(url: URL) {
self.url = url
}
func fetchImage(callback: #escaping (UIImage?) -> Void) {
if let img = self._imageCache {
callback(img)
return
}
let task = URLSession.shared.dataTask(with: self.url, completionHandler: { data, _, _ in
if let imageData = data, let img = UIImage(data: imageData) {
self._imageCache = img
callback(img)
} else {
callback(nil)
}
})
task.resume()
}
}

Images are not displayed because you download them in background thread (asynchronously), and loadImages() is called synchronously. That means loadImage() is called before myModel.mainPulatesData() is executed, so when your images are downloaded, tableview is not being updated (reloadData() is not called). You should create Protocol to notify UIViewController, that data has been downloaded, or use Completion Handler.
Simple example of handler I am using, I call this in my viewDidLoad, it requests data from server and return an array of [Reservation]?
ReservationTableModule.requestAllReservations { [weak self] reservations in
guard let `self` = self else {
return
}
guard let `reservations` = `reservations` else {
return
}
self.reservations = reservations
.reservationsTableView.reloadData()
}
this is actual request function
class func requestAllReservations(handler: #escaping ([Reservation]?) -> Void) {
let url = "reservations/all"
APIModel.shared.requestWithLocation(.post, URL: url, parameters: nil) { data in
let reservations = data?["reservations"].to(type: Reservation.self) as? [Reservation]
handler(reservations)
}
}
handler: #escaping ([Reservation]?) -> Void is called completion handler, you should, I guess make it handler: #escaping ([UIImage]?) -> Void and after your data downloaded call handler(reservations)

You should take note of the sequence of your function call here:
myModel.mainPulatesURLs() --> populates myURLs
myModel.mainPulatesData() --> populates image from myURLs in forloop asynchronously.
loadImages() --> called asynchronously.
while you're loading your images from myModel.mainPulatesData() you already called loadImages() which myModel.imageList was still empty.
you should call loadImages() after a callback from myModel.mainPulatesData() or when you're sure that the images where already loaded.
you can use dispatch_group_t to configure the callbacks.
here as requested:
import UIKit
var myURLs: [String] = ["urlA", "urlB", "urlC", "urlD"]
// we define the group for our asynchronous fetch. also works in synchronous config
var fetchGroup = DispatchGroup()
for urlString in myURLs {
// for every url fetch we define, we will call an 'enter' to issue that there is a new block for us to wait or monitor
fetchGroup.enter()
// the fetch goes here
let url = URL(string: urlString)!
URLSession.shared.downloadTask(with: URLRequest(url: url), completionHandler: { (urlReq, urlRes, error) in
// do your download config here...
// now that the block has downloaded the image, we are to notify that it is done by calling 'leave'
fetchGroup.leave()
}).resume()
}
// now this is where our config will be used.
fetchGroup.notify(queue: DispatchQueue.main) {
// reload your table here as all of the image were fetched regardless of download error.
}

Related

How to properly display parsed data in table view after receiving them through completion handler

I need to show info about movies(taken from https://developers.themoviedb.org/) in tableView. I'm doing network request using a singleton and then pass parsed data to tableViewController through completion handler. I can print received data but I can't properly set them in tableView cell. Could you please help me how to fix this problem.
Network Manager
func getMovies(completion: #escaping ([Movies]?) -> Void) {
guard let url = URL(string: "https://api.themoviedb.org/3/movie/now_playing?api_key=\(apiKey)&language=en")
else { fatalError("Wrong URL") }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let jsonData = data {
let decoder = JSONDecoder()
do {
let moviesResult = try decoder.decode(MoviesResult.self, from: jsonData)
let movies = moviesResult.results
completion(movies)
}
catch {
print(error)
}
}
}.resume()
}
Movies View Controller
var movies = [Movies]()
override func viewDidLoad() {
super.viewDidLoad()
network.getMovies { result in
if let result = result {
self.movies = result
print(self.movies)
}
}
extension MoviesViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return movies.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let movie = movies[indexPath.row]
print(movie)
if let cell = tableView.dequeueReusableCell(withIdentifier: "moviesMainInfo", for: indexPath) as? MovieTableViewCell {
cell.filmTitle.text = movie.title
cell.filmRating.text = String(movie.popularity!)
return cell
}
return UITableViewCell()
}
}
Parsed result: [MovieApp.Movies(genreIDs: Optional([14, 28, 12]), overview: Optional("Wonder Woman comes into conflict with the Soviet Union during the Cold War in the 1980s and finds a formidable foe by the name of the Cheetah."), popularity: Optional(1927.057), title: Optional("Wonder Woman 1984"), releaseDate: Optional("2020-12-16"), posterPath: Optional("/8UlWHLMpgZm9bx6QYh0NFoq67TZ.jpg")),
You are doing everything correctly, you just need to reload your UITableView when data arrives. Be aware that you need to reload your UITableView on the main thread, because UIKit isn't thread safe:
otherwise your application will most probably crash:
private func reloadTableView() {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Also I encourage you to extract your networking function from viewDidLoad. An other improvement is to use [weak self] in your closures to avoid memory leaks:
private func loadData() {
network.getMovies { [weak self] result in
if let result = result {
self?.movies = result
print(self?.movies)
self?.reloadTableView()
} else {
// Maybe show some info that the data could not be fetched
}
}
}
And in your viewDidLoad just call it:
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}

Adding image from api to CustomCell programmatically is getting laggy

My tableview gets slow when scrolling, I have a custom cell and a tableview:
I have this controller, where the api call is made and the array of trips is filled, then in cellForRowAt im creating the cell
class HistorialViewController: UIViewController , UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var historialTableView: UITableView!
var trips = [RootClass]()
override func viewDidLoad() {
super.viewDidLoad()
self.historialTableView.delegate = self
self.historialTableView.dataSource = self
self.historialTableView.register(CustomCellHistorial.self, forCellReuseIdentifier: cellId)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("coming back")
self.fetchTrips()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.historialTableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! CustomCellHistorial
let trip = self.trips[indexPath.row]
cell.trip = trip
return cell
}
private func fetchTrips(){
AFWrapper.getTripHistory( success: { (jsonResponse) in
self.trips = []
for item in jsonResponse["trips"].arrayValue {
self.trips.append(RootClass(fromJson:item))
}
self.reloadTrips()
Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { (nil) in
self.indicator.stopAnimating()
}
}, failure: { (error) -> Void in
print(error)
})
}
func reloadTrips(){
DispatchQueue.main.async{
self.historialTableView.reloadData()
}
}
This is my CustomCell
class CustomCellHistorial : UITableViewCell{
var trip: RootClass? {
didSet{
dateTimeLabel.text = returnCleanDate(fecha: trip!.createdAt)
distanceAndTimeLabel.text = returnDistanceAndTime(distance: (trip?.trip!.real!.dist!)!, time: (trip?.trip!.real!.time)!)
priceLabel.text = returnCleanPrice(price: (trip?.trip!.real!.price!)!)
ratedLabel.text = "Not Rated"
self.productImage.image = self.returnDriverImage(photoUrl: (self.trip?.driver!.photo!)!)
if (trip!.score) != nil {
let score = trip!.score
if (score?.driver) != nil{
if(trip!.score!.driver!.stars! != 0.0 ){
ratedLabel.isHidden = true
}else{
ratedLabel.isHidden = false
}
}else{
print("yei")
}
}
}
}
private func returnDriverImage(photoUrl: String) -> UIImage{
let url = URL(string: photoUrl)
do {
let data = try Data(contentsOf: url!)
if let roundedimg = UIImage(data: data){
let croppedImageDriver = roundedimg.resize(toTargetSize: self.productImage.frame.size)
return croppedImageDriver
}
} catch let error {
debugPrint("ERRor :: \(error)")
let image = UIImage(named: "perfilIcono")
return image!
}
let image = UIImage(named: "perfilIcono")
return image!
}
Answers that I have found are for older versions of Swift, and the way they make the tableview its in storyboard or they are not handling custom cells.
I think the problem is in the returnDriverImage function.
This line
let data = try Data(contentsOf: url!)
You call from
self.productImage.image = self.returnDriverImage(photoUrl: (self.trip?.driver!.photo!)!)
blocks the main thread and re downloads the same image multiple times when scroll , please consider using SDWebImage

UICollectionView swift 3 laggy scroll UIImage

I am experiencing very choppy scrolling in my collection view. There are only 10 cells. It is because of my method of retrieving the images which is to take the URL and turn it to UIImage data.
Images variable is just an array of image URLs.
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "mediaCells", for: indexPath) as! MediaCell
let url = URL(string: images[indexPath.row] as! String)
let data = try? Data(contentsOf: url!)
cell.mediaImage.image = UIImage(data: data!)
return cell
}
A solution I found was to do this first for all images. The problem with this approach is that it will have to wait for all images to download the data and append to the array before we display the collectionView. This can take quite a few seconds 10-15 seconds before we can render the collection. Too slow!
override func viewDidLoad() {
super.viewDidLoad()
Alamofire.request(URL(string: "myURL")!,
method: .get)
.responseJSON(completionHandler: {(response) -> Void in
if let value = response.result.value{
let json = JSON(value).arrayValue
for item in json{
let url = URL(string: item["image"].string!)
let data = try? Data(contentsOf: url!)
self.images.append(data)
}
self.collectionView.reloadData()
}
})
}
Used SDWebImage from cocoapods to do Async image fetching.
https://cocoapods.org/?q=SDWebImage
cell.mediaImage.sd_setImage(with: URL(string: images[indexPath.row] as! String))
Some will shout about the use of globals, but this is the way I did previously:
Declare a global array of UIImage
var images: [UIImage] = []
When image is downloaded, append it to that array
for item in json{
let url = URL(string: item["image"].string!)
let data = try? Data(contentsOf: url!)
let image = UIImage(data: data!)
images.append(image)
NotificationCenter.default.post(Notification.Name(rawValue: "gotImage"), nil)
}
Follow the Notification in your VC
override function viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(collectionView.reloadData()), name: NSNotification.Name(rawValue: "gotImage"), object: nil)
super.viewDidLoad()
}
Do not assign an image if not available in your cell for collectionView
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
.....
if let image = images[indexPath].row {
cell.mediaImage.image = image
}
....
}
PS: If all the downloading and loading action happens in the same VC, just declare images inside your VC class.
Seems like you need to do the lazy loading of images in your collection view based on the visible cells only. I have implemented the same approach like this.
1) First steps you need to create a class named it as PendingOperations.swift
class PendingOperations {
lazy var downloadsInProgress = [IndexPath: Operation]()
lazy var downloadQueue: OperationQueue = {
var queue = OperationQueue()
queue.name = "downloadQueue"
queue.maxConcurrentOperationCount = 1
return queue
}()
}
2) Seconds steps create a class named it as ImageDownloader where you can make your network call to download the image.
class ImageDownloader: Operation {
let imageId: String
init(imageId: String) {
self.imageId = imageId
}
override func main() {
if self.isCancelled {
return
}
Alamofire.request(URL(string: "myURL")!,
method: .get)
.responseJSON(completionHandler: {(response) -> Void in
if let value = response.result.value{
let json = JSON(value).arrayValue
for item in json{
let url = URL(string: item["image"].string!)
self.image = try? Data(contentsOf: url!)
})
if self.isCancelled {
return
}
}
}
3) Third steps implement some methods inside viewController to manipulate the operations based on the visible cells.
func startDownload(_ image: imageId, indexPath: IndexPath) {
if let _ = pendingOperations.downloadsInProgress[indexPath] {
return
}
let downloader = ImageDownloader(image: imageId)
downloader.completionBlock = {
if downloader.isCancelled {
return
}
}
pendingOperations.downloadsInProgress.removeValue(forKey: indexPath)
pendingOperations.downloadsInProgress[indexPath] = downloader
pendingOperations.downloadQueue.addOperation(downloader)
}
}
func suspendAllOperation() {
pendingOperations.downloadQueue.isSuspended = true
}
func resumeAllOperation() {
pendingOperations.downloadQueue.isSuspended = false
}
func loadImages() {
let pathArray = collectionView.indexPathsForVisibleItems
let allPendingOperations =
Set(Array(pendingOperations.downloadsInProgress.keys))
let visiblePath = Set(pathArray!)
var cancelOperation = allPendingOperations
cancelOperation.subtract(visiblePath)
var startOperation = visiblePath
startOperation.subtract(allPendingOperations)
for indexpath in cancelOperation {
if let pendingDownload =
pendingOperations.downloadsInProgress[indexpath] {
pendingDownload.cancel()
}
pendingOperations.downloadsInProgress.removeValue(forKey: indexpath)
}
for indexpath in startOperation {
let indexPath = indexpath
startDownload(imageId, indexPath: indexPath)
}
}
4) Fourth steps to implement the scrollView delegate methods.
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
suspendAllOperation()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
loadImages()
resumeAllOperation()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
loadImages()
resumeAllOperation()
}
}
func clearImageDownloaderOperations() {
if pendingOperations.downloadsInProgress.count > 0 {
pendingOperations.downloadsInProgress.removeAll()
pendingOperations.downloadQueue.cancelAllOperations()
}
}
Using Kingfisher 5 for prefetching, loading, showing and caching images we can solve the issue.
let url = URL(fileURLWithPath: path)
let provider = LocalFileImageDataProvider(fileURL: url)
imageView.kf.setImage(with: provider)
https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet#image-from-local-file

Delete, add and display data swift table view controller

Im my Swift app, I need to display data in a tableview. The data at server side changes often, and therefore I need to delete any previous data and add the new data fetched. I am able to delete and fetch and I can see the new data in the console as well as realm browser. However, the data is not available in the table view controller from Results.
class ServiceProvidersTableViewController: UITableViewController {
// MARK: - Data Source
var serviceSubCategory: String = ""
var serviceSubCategories: Results<ServiceSubCategory>?
// MARK: - Realm
let realm = try! Realm()
let fetchData = FetchData()
override func viewDidLoad() {
super.viewDidLoad()
self.deleteSubCategories()
FetchData.get(ServiceSubCategory.self, params: ["subCategoryId": serviceSubCategory], success: {
self.infoAlert("Service Categories", message: "Fetch Successful")
}) {
self.infoAlert("Failed", message: "Fetch Failed")
}
self.fetchSubCategories()
}
func deleteSubCategories()
{
let subCategories = realm.objects(ServiceSubCategory.self)
print("Before delete \(subCategories)")
try! realm.write{
realm.delete(subCategories)
}
print("After delete \(subCategories)")
}
func fetchSubCategories() {
self.realm.refresh()
self.serviceSubCategories = self.realm.objects(ServiceSubCategory)
tableView.reloadData()
print("After fetch \(self.serviceSubCategories)")
}
struct Storyboard {
static let CellIndentifier = "ServiceProvidersCell"
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
print(self.serviceSubCategories!.count)
return self.serviceSubCategories!.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(Storyboard.CellIndentifier, forIndexPath: indexPath) as! ServiceProvidersTableViewCell
print(serviceSubCategories)
cell.serviceSubCategories = self.serviceSubCategories![indexPath.row]
return cell
}
func infoAlert(title:String,message:String) -> Void {
let actionSheetController: UIAlertController = UIAlertController(title:title, message:message, preferredStyle: .Alert)
let cancelAction: UIAlertAction = UIAlertAction(title: "Cancel", style: .Cancel) { action -> Void in }
actionSheetController.addAction(cancelAction)
self.presentViewController(actionSheetController, animated: true, completion: nil)
}
}
My guess is that you need to delete the object from the local "serviceSubCategory" and not just from the server.
Put a tableView.reloadData() everytime the server listener fires.
Probably in the fetchSubCategories() and deleteSubCategories() functions.
I solved (I should say, I am dumb) as follows:-
The FetchData method has a callback success, which in the example I used displays an alert.
I created another function in the tableViewController and called this in the success call back. Inside the new method I called self.tableView.reloadData().
Thanks to all for helping me out

NSOperation for loading image to TableView

Here is my Movie class:
import UIKit
class Movie {
var title: String = ""
var rating: Double = 0
var image: UIImage = UIImage()
}
I want to load the the array of movie to tableView, I have tried like this:
import UIKit
import Cosmos
import Alamofire
//import AlamofireImage
import SwiftyJSON
class Downloader {
class func downloadImageWithURL(url:String) -> UIImage! {
let data = NSData(contentsOfURL: NSURL(string: url)!)
return UIImage(data: data!)
}
}
class MovieViewController: UIViewController, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
var movies: [Movie] = []
override func viewDidLoad() {
super.viewDidLoad()
fetchMovies()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! MovieTableViewCell
let movie = movies[indexPath.row]
cell.movieTitleLabel.text = movie.title
cell.movieRatingView.rating = Double(movie.rating / 20)
cell.movieImageView.image = movie.image
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return movies.count
}
func fetchMovies() {
let movieURLString = "https://coderschool-movies.herokuapp.com/movies?api_key=xja087zcvxljadsflh214"
Alamofire.request(.GET, movieURLString).responseJSON { response in
let json = JSON(response.result.value!)
let movies = json["movies"].arrayValue
let queue = NSOperationQueue()
for movie in movies {
let title = movie["title"].string
let rating = movie["ratings"]["audience_score"].double
let imageURLString = movie["posters"]["thumbnail"].string
let movie = Movie()
movie.title = title!
movie.rating = rating!
let operation = NSBlockOperation(block: {
movie.image = Downloader.downloadImageWithURL(imageURLString!)
})
queue.addOperation(operation)
self.movies.append(movie)
self.tableView.reloadData()
}
}
}
}
The problem is that: when I scroll down or up, the images will be reload, otherwise they are not reloaded. Only the title and rating will be
I only know the reason is these line
self.movies.append(movie)
self.tableView.reloadData()
are compiled before
let operation = NSBlockOperation(block: {
movie.image = Downloader.downloadImageWithURL(imageURLString!)
})
queue.addOperation(operation)
But if I scroll down, it will like this:
I've already do it perfectly with AlamofireImage, but I really want to use NSOperation for diving with it.
I think you should make sure to reloadData() on the main thread. By default it's happening on a background thread and cannot update the UI.
dispatch_async(dispatch_get_main_queue(),{
self.tableView.reloadData()
})
I solved the problem by put the reloadData to the main thread, like so:
let operation = NSBlockOperation(block: {
movie.image = Downloader.downloadImageWithURL(imageURLString!)
NSOperationQueue.mainQueue().addOperationWithBlock() {
self.tableView.reloadData()
}
})
Now it works well.