If someone have good idea to implement this let me know
thanks in adance
class ViewController: UIViewController,UITableViewDataSource,UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var tableViewCell: UITableViewCell!
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingCell", for: indexPath)
print(self.array[indexPath.row])
cell.textLabel?.text = (self.array[indexPath.row]["title"] as! String)
cell.detailTextLabel?.text = (self.array[indexPath.row]["username"] as! String)
Alamofire.request(imageUrl!, method: .get).response { response in
guard let image = UIImage(data:response.data!) else {
// Handle error
return
}
let imageData = UIImageJPEGRepresentation(image,1.0)
cell.myImage.image = UIImage(data : imageData!)
}
return cell
}
}
You are force unwrapping some values. Try to safely unwrap the values instead. You still have to figure out why the values are nil though.
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
if let url = NSURL(string: self.restaurants[indexPath.row].restaurantImage), let data = NSData(contentsOfURL: url), let image = UIImage(data: data) {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
cell.restaurantImage.image = image
}
}
})
This will help you figure out where the exact issue is
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
guard indexPath.row < self.restaurants.count
else
{
print("Indexpath greater than restaurants count")
return
}
guard let imageUrl = NSURL(string:self.restaurants[indexPath.row].restaurantImage)
else
{
print("Image url is empty")
return
}
guard let imageData = NSData(contentsOfURL: imageUrl)
else
{
print("Cannot retreive data at url")
return
}
guard let image = UIImage(data: imageData)
else
{
print("Data doesnt contain image")
return
}
dispatch_async(dispatch_get_main_queue()) { () -> Void in
cell.restaurantImage.image = image
}
})
my collection view don't show gifs.. im using GIPHY.. and SwiftGif Extension, to show the gifs on UIImageView... this is the code
func searchGif(search: String) {
GiphyCore.configure(apiKey: "hRuR15WOxvhonLAsLhd0R8pDGvJxQYOk")
respondView.isHidden = true
_ = GiphyCore.shared.search(search, media: .gif, offset: 2, limit: 6, rating: .ratedG, lang: .english, completionHandler: { [weak self] (response, error) in
self?.isDataLoading = false
if let error = error {
print("error in response", error)
}
guard
let data = response?.data else { return }
self?.initialize()
for results in data {
let urlString = results.url
guard let url = URL(string: urlString) else { return }
do {
let data = try Data(contentsOf: url)
let foundedGif = GifModel(gifUrl: data, urlString: urlString)
self?.gifModel.append(foundedGif)
} catch let error as NSError {
print(error)
}
}
if self?.gifModel.isEmpty ?? false {
self?.setupNofound()
}
DispatchQueue.main.async {
self?.gifCollectionView.reloadData()
}
})
}
in the delegates on collection view...
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GifSearchCollectionViewCell.identifier,
for: indexPath
) as? GifSearchCollectionViewCell else {
return UICollectionViewCell()
}
cell.gifModel = gifModel[indexPath.row]
return cell
}
and in the numberOfItems in sections as well .....
I put gifModel.count the data works good, I have a response with the 6 on the array model...
and in the cell:
#IBOutlet weak var splashGifView: UIImageView!
var gifModel: GifModel? {
didSet {
guard
let gif = gifModel?.gifUrl else { return }
splashGifView.image = UIImage.gifImageWithData(gif)
}
}
I tried with String but nothing, the cells already create, but don't show the gifs... someone can help?
update...
#IBOutlet weak var splashGifView: UIImageView!
var gifModel: GifModel? {
didSet {
guard let gif = gifModel? { return }
let url = gif.gifUrl. // <- this give nill
splashGifView.image = UIImage.gifImageWithData(url)
}
}
the url have nill, but in my model I have the data url correctly...
I figured out!.. GIPHY, have a struct very "inbound", I get the image Gif inside of the response like this....
results.images?.original?.gifUrl
for results in data {
let newGif = GifModel(gifUrl: results.images?.original?.gifUrl ?? "", run: false)
self?.gifModel.append(newGif)
}
and now I can get the url with the extension ".GIF" and with that SwiftGif can show on the collectionView the gifs...
I am new to parsing json in Swift and in my app I created an inbox. In this inbox, I load a profile image and a name in every cell. I found an API online with video game characters and their images for a test. However, when the json is parsed and put in the cell, the app loads the cells and occasionally the images move around to other cells or duplicate. I have seen this posted before, but none of the past answers have solved my solution.
Here is what it looked like when it loaded which is incorrect
Here is what happened one second later which is still incorrect and you can see duplication
This is my CollectionViewCell.Swift File
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var imageCell: UIImageView!
#IBOutlet weak var dateCell: UILabel!
override func prepareForReuse() {
self.imageCell.image = nil
self.imageCell.setNeedsDisplay() // tried adding after some recommendations
self.setNeedsDisplay() // tried adding after some recommendations
super.prepareForReuse()
}
}
This is my main inbox view controller extension for the image I found online
extension UIImageView {
func downloadedFrom(url: URL, contentMode mode: UIViewContentMode = .scaleAspectFit) {
contentMode = mode
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 }
DispatchQueue.main.async() {
self.image = image
}
}.resume()
}
func downloadedFrom(link: String, contentMode mode: UIViewContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloadedFrom(url: url, contentMode: mode)
}
}
This is the rest of the inbox view controller code (deleted unrelated code from it for the purpose of this question)
class InboxViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var inboxCollection: UICollectionView!
struct Hero: Decodable {
let localized_name: String
let img: String
}
var heroes = [Hero]()
override func viewDidLoad() {
super.viewDidLoad()
inboxCollection.dataSource = self
let url = URL(string: "https://api.opendota.com/api/heroStats")
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error == nil {
do {
self.heroes = try JSONDecoder().decode([Hero].self, from: data!)
}catch {
print("Parse Error")
}
DispatchQueue.main.async {
self.inboxCollection.reloadData()
}
}
}.resume()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.heroes.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as! CollectionViewCell
cell.dateCell.text = heroes[indexPath.row].localized_name.capitalized
let defaultLink = "https://api.opendota.com"
cell.imageCell.image = nil
let completelink = defaultLink + heroes[indexPath.row].img
cell.imageCell.image = nil
cell.imageCell.downloadedFrom(link: completelink)
cell.imageCell.clipsToBounds = true
cell.imageCell.layer.cornerRadius = cell.imageCell.frame.height / 2
cell.imageCell.contentMode = .scaleAspectFill
cell.imageCell.image = nil
return cell
}
You shouldn't be setting UIImageView's image within the UIImageView. In fact, don't even create two separate methods - you have no good reason to. Download each image in your UIViewController and use a dictionary to map a hero's name to their image like so:
var heroImages = [String:URL]()
func getDataFromUrl(url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url) { data, response, error in
completion(data, response, error)
}.resume()
}
func getImages() {
for hero in self.heroes {
let url = NSURL(string: hero)
getDataFromURL(url: url, completion: {(data: Data?, response:URLResponse?, error: Error?) in
if (data != nil) {
image = UIImage(data: data)
heroImages[hero] = image }
if (heroImages.count == self.heroes.count) {
// We've downloaded all the images, update collection view
DispatchQueue.main.async { self.collectionView.reloadData() }
}
}
}
I have a tableview that populates its cells with data from firebase. Each cell posses an image and each image is loaded asynchronously and cached. The problem is that random cells will use the wrong image, although the other data in the cell is correct. If I scroll so that the cell goes off view, the correct image then loads.
Research:
Images in UITableViewCells are loading wrong (Dont really understand Obj C)
What is the correct way to use prepareForReuse?
Image Cache:
import Foundation
import UIKit
let imageCache = NSCache<NSString, AnyObject>()
extension UIImageView {
func loadImageUsingCache(urlString: String) {
self.image = #imageLiteral(resourceName: "logo3")
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: {(data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
self.image = downloadedImage
}
})
}).resume()
}
}
Tableview Cell
import Foundation
import Firebase
class GroupCell: UITableViewCell {
var group: Groups!
var members = [String]()
#IBOutlet weak var groupImage: UIImageView!
#IBOutlet weak var groupName: UILabel!
#IBOutlet weak var groupRep: UILabel!
#IBOutlet weak var memberCount: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
func configureCell(group: Groups) {
self.group = group
self.groupName.text = group.name
self.groupRep.text = "\(group.groupScore)"
if let groupImage = group.groupImage {
self.groupImage.loadImageUsingCache(urlString: groupImage)
} else {
self.groupImage.image = //random image
}
for member in group.members {
self.members.append(member.key)
}
self.memberCount.text = "\(members.count)"
}
}
TableView
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "groupCell") as? GroupCell {
let group = FriendSystem.system.activeGroups[indexPath.row]
cell.configureCell(group: group)
return cell
}
return UITableViewCell()
}
Sounds like you need to add the prepareForReuse method to your GroupCell class.
In that method add self.groupImage.image = nil It will reset your image view to empty until the correct image is set.
I'm trying to async load pictures inside my FriendsTableView (UITableView) cell. The images load fine but when I'll scroll the table the images will change a few times and wrong images are getting assigned to wrong cells.
I've tried all methods I could find in StackOverflow including adding a tag to the raw and then checking it but that didn't work. I'm also verifying the cell that should update with indexPath and check if the cell exists. So I have no idea why this is happening.
Here is my code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("friendCell", forIndexPath: indexPath) as! FriendTableViewCell
var avatar_url: NSURL
let friend = sortedFriends[indexPath.row]
//Style the cell image to be round
cell.friendAvatar.layer.cornerRadius = 36
cell.friendAvatar.layer.masksToBounds = true
//Load friend photo asyncronisly
avatar_url = NSURL(string: String(friend["friend_photo_url"]))!
if avatar_url != "" {
getDataFromUrl(avatar_url) { (data, response, error) in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
guard let data = data where error == nil else { return }
let thisCell = tableView.cellForRowAtIndexPath(indexPath)
if (thisCell) != nil {
let updateCell = thisCell as! FriendTableViewCell
updateCell.friendAvatar.image = UIImage(data: data)
}
}
}
}
cell.friendNameLabel.text = friend["friend_name"].string
cell.friendHealthPoints.text = String(friend["friend_health_points"])
return cell
}
On cellForRowAtIndexPath:
1) Assign an index value to your custom cell. For instance,
cell.tag = indexPath.row
2) On main thread, before assigning the image, check if the image belongs the corresponding cell by matching it with the tag.
dispatch_async(dispatch_get_main_queue(), ^{
if(cell.tag == indexPath.row) {
UIImage *tmpImage = [[UIImage alloc] initWithData:imgData];
thumbnailImageView.image = tmpImage;
}});
});
This is because UITableView reuses cells. Loading them in this way causes the async requests to return at different time and mess up the order.
I suggest that you use some library which would make your life easier like Kingfisher. It will download and cache images for you. Also you wouldn't have to worry about async calls.
https://github.com/onevcat/Kingfisher
Your code with it would look something like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("friendCell", forIndexPath: indexPath) as! FriendTableViewCell
var avatar_url: NSURL
let friend = sortedFriends[indexPath.row]
//Style the cell image to be round
cell.friendAvatar.layer.cornerRadius = 36
cell.friendAvatar.layer.masksToBounds = true
//Load friend photo asyncronisly
avatar_url = NSURL(string: String(friend["friend_photo_url"]))!
if avatar_url != "" {
cell.friendAvatar.kf_setImageWithURL(avatar_url)
}
cell.friendNameLabel.text = friend["friend_name"].string
cell.friendHealthPoints.text = String(friend["friend_health_points"])
return cell
}
UPDATE
There are some great open source libraries for image caching such as KingFisher and SDWebImage. I would recommend that you try one of them rather than writing your own implementation.
END UPDATE
So there are several things you need to do in order for this to work. First let's look at the caching code.
// Global variable or stored in a singleton / top level object (Ex: AppCoordinator, AppDelegate)
let imageCache = NSCache<NSString, UIImage>()
extension UIImageView {
func downloadImage(from imgURL: String) -> URLSessionDataTask? {
guard let url = URL(string: imgURL) else { return nil }
// set initial image to nil so it doesn't use the image from a reused cell
image = nil
// check if the image is already in the cache
if let imageToCache = imageCache.object(forKey: imgURL as NSString) {
self.image = imageToCache
return nil
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let err = error {
print(err)
return
}
DispatchQueue.main.async {
// create UIImage
let imageToCache = UIImage(data: data!)
// add image to cache
imageCache.setObject(imageToCache!, forKey: imgURL as NSString)
self.image = imageToCache
}
}
task.resume()
return task
}
}
You can use this outside of a TableView or CollectionView cell like this
let imageView = UIImageView()
let imageTask = imageView.downloadImage(from: "https://unsplash.com/photos/cssvEZacHvQ")
To use this in a TableView or CollectionView cell you'll need to reset the image to nil in prepareForReuse and cancel the download task. (Thanks for pointing that out #rob
final class ImageCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
private var task: URLSessionDataTask?
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
task = nil
imageView.image = nil
}
// Called in cellForRowAt / cellForItemAt
func configureWith(urlString: String) {
if task == nil {
// Ignore calls when reloading
task = imageView.downloadImage(from: urlString)
}
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "imageCell", for: indexPath) as! ImageCell
cell.configureWith(urlString: "https://unsplash.com/photos/cssvEZacHvQ") // Url for indexPath
return cell
}
Keep in mind that even if you use a 3rd party library you'll still want to nil out the image and cancel the task in prepareForReuse
If targeting iOS 13 or later, you can use Combine and dataTaskPublisher(for:). See WWDC 2019 video Advances in Networking, Part 1.
The idea is to let the cell keep track of the “publisher”, and have prepareForReuse:
cancel the prior image request;
set the image property of the image view to nil (or a placeholder); and then
start another image request.
For example:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let url = ...
cell.setImage(to: url)
return cell
}
}
class CustomCell: UITableViewCell {
#IBOutlet weak var customImageView: UIImageView!
private var subscriber: AnyCancellable?
override func prepareForReuse() {
super.prepareForReuse()
subscriber?.cancel()
customImageView?.image = nil
}
func setImage(to url: URL) {
subscriber = ImageManager.shared.imagePublisher(for: url, errorImage: UIImage(systemName: "xmark.octagon"))
.assign(to: \.customImageView.image, on: self)
}
}
Where:
class ImageManager {
static let shared = ImageManager()
private init() { }
private let session: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .returnCacheDataElseLoad
let session = URLSession(configuration: configuration)
return session
}()
enum ImageManagerError: Error {
case invalidResponse
}
func imagePublisher(for url: URL, errorImage: UIImage? = nil) -> AnyPublisher<UIImage?, Never> {
session.dataTaskPublisher(for: url)
.tryMap { data, response in
guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode,
let image = UIImage(data: data)
else {
throw ImageManagerError.invalidResponse
}
return image
}
.replaceError(with: errorImage)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
If targeting earlier iOS versions, rather than using Combine, you can use URLSession, with the same idea of canceling the prior request in prepareForReuse:
class CustomCell: UITableViewCell {
#IBOutlet weak var customImageView: UIImageView!
private weak var task: URLSessionTask?
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
customImageView?.image = nil
}
func setImage(to url: URL) {
task = ImageManager.shared.imageTask(for: url) { result in
switch result {
case .failure(let error): print(error)
case .success(let image): self.customImageView.image = image
}
}
}
}
Where:
class ImageManager {
static let shared = ImageManager()
private init() { }
private let session: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .returnCacheDataElseLoad
let session = URLSession(configuration: configuration)
return session
}()
enum ImageManagerError: Error {
case invalidResponse
}
#discardableResult
func imageTask(for url: URL, completion: #escaping (Result<UIImage, Error>) -> Void) -> URLSessionTask {
let task = session.dataTask(with: url) { data, response, error in
guard let data = data else {
DispatchQueue.main.async { completion(.failure(error!)) }
return
}
guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode,
let image = UIImage(data: data)
else {
DispatchQueue.main.async { completion(.failure(ImageManagerError.invalidResponse)) }
return
}
DispatchQueue.main.async { completion(.success(image)) }
}
task.resume()
return task
}
}
Depending on the implementation there can be many things that will cause all of the answers here to not work (including mine). Checking the tag did not work for me, checking the cache neither, i have a custom Photo class that carries the full image, thumbnail and more data, so i have to take care of that too and not just prevent the image from being reused improperly. Since you will probably be assigning the images to the cell imageView after they're done downloading, you will need to cancel the download and reset anything you need on prepareForReuse()
Example if you're using something like SDWebImage
override func prepareForReuse() {
super.prepareForReuse()
self.imageView.sd_cancelCurrentImageLoad()
self.imageView = nil
//Stop or reset anything else that is needed here
}
If you have subclassed the imageview and handle the download yourself make sure you setup a way to cancel the download before the completion is called and call the cancel on prepareForReuse()
e.g.
imageView.cancelDownload()
You can cancel this from the UIViewController too. This on itself or combined with some of the answers will most likely solve this issue.
I solve the problem just implementing a custom UIImage class and I did a String condition as the code below:
let imageCache = NSCache<NSString, UIImage>()
class CustomImageView: UIImageView {
var imageUrlString: String?
func downloadImageFrom(withUrl urlString : String) {
imageUrlString = urlString
let url = URL(string: urlString)
self.image = nil
if let cachedImage = imageCache.object(forKey: urlString as NSString) {
self.image = cachedImage
return
}
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
if let image = UIImage(data: data!) {
imageCache.setObject(image, forKey: NSString(string: urlString))
if self.imageUrlString == urlString {
self.image = image
}
}
}
}).resume()
}
}
It works for me.
TableView reuses cells. Try this:
import UIKit
class CustomViewCell: UITableViewCell {
#IBOutlet weak var imageView: UIImageView!
private var task: URLSessionDataTask?
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
imageView.image = nil
}
func configureWith(url string: String) {
guard let url = URL(string: string) else { return }
task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
task?.resume()
}
}
Because TableView reuses cells. In your cell class try this code:
class CustomViewCell: UITableViewCell {
#IBOutlet weak var catImageView: UIImageView!
private var task: URLSessionDataTask?
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
catImageView.image = nil
}
func configureWith(url string: String) {
guard let url = URL(string: string) else { return }
task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.catImageView.image = image
}
}
}
task?.resume()
}
}
the Best Solution for This Problem i have is for Swift 3 or Swift 4
Simply write these two lines
cell.videoImage.image = nil
cell.thumbnailimage.setImageWith(imageurl!)
Swift 3
DispatchQueue.main.async(execute: {() -> Void in
if cell.tag == indexPath.row {
var tmpImage = UIImage(data: imgData)
thumbnailImageView.image = tmpImage
}
})
I created a new UIImage variable in my model and load the image/placeholder from there when creating a new model instance. It worked perfectly fine.
It is an example that using Kingfisher caching at memory and disk after downloaded.
It replace UrlSession downloading traditional and avoid re-download UIImageView after scroll down TableViewCell
https://gist.github.com/andreconghau/4c3b04205195f452800d2892e91a079a
Example Output
sucess
Image Size:
(460.0, 460.0)
Cache:
disk
Source:
network(Kingfisher.ImageResource(cacheKey: "https://avatars0.githubusercontent.com/u/5936?v=4", downloadURL: https://avatars0.githubusercontent.com/u/5936?v=4))
Original source:
network(Kingfisher.ImageResource(cacheKey: "https://avatars0.githubusercontent.com/u/5936?v=4", downloadURL: https://avatars0.githubusercontent.com/u/5936?v=4))