Returning UIImage Async using Task - swift

I have a UICollectionView of cells each including an image.
I want to load the cell's images.
My code that instantiate the image :
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "product_collection_cell", for: indexPath) as! ProductsCollectionViewCell
let prodInCell = searchActive ? filtered[indexPath.row] : products[indexPath.row]
// Set fields
cell.ProductImageView.image = prodInCell.GetProductImage()
cell.ProductName.text = prodInCell.Name()
cell.ProductPrice.text = String(prodInCell.Price())
cell.productUniqueID = prodInCell.UniqueID()
return cell
}
My Product's GetProductImage function:
public func GetProductImage() -> UIImage
{
let prodID = self.UniqueID()
let dbRef = Storage.storage().reference().child(prodID).child("pic0.jpg")
var prodImg = #imageLiteral(resourceName: "DefaultProductImage")
let imgTask = dbRef.getData(maxSize: 10*1024*1024, completion: // Up to 10 MB pictures
{
(data, error) in
if let data = data
{
if let img = UIImage(data: data)
{
prodImg = img
}
}
})
imgTask.observe(.progress, handler: {(snapshot) in
print (snapshot.progress ?? "NO MORE PROGRESS")
})
imgTask.resume()
return prodImg
}
I want a UIImage to be retrieved from Firebase Storage, or return a DefaultProductImage if none exists. Current implementation stucks my UI and seems to not really load anything from Firebase.
How do I Make this work ? I would also like for it to not take so much time - so perhaps using a couple of tasks to each load an image would be a good solution.
Edit :
This is my code now :
accept
You can use a completion block to return the UIImage asynchronously.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "product_collection_cell", for: indexPath) as! ProductsCollectionViewCell
let prodInCell = searchActive ? filtered[indexPath.row] : products[indexPath.row]
// Set fields
cell.ProductImageView.image = #imageLiteral(resourceName: "DefaultProductImage")
prodInCell.GetProductImage() { image in
cell.ProductImageView.image = image
}
cell.ProductName.text = prodInCell.Name()
cell.ProductPrice.text = String(prodInCell.Price())
cell.productUniqueID = prodInCell.UniqueID()
return cell
}
public func GetProductImage(completion: ((UIImage?) -> Void)) {
let prodID = self.UniqueID()
let dbRef = Storage.storage().reference().child(prodID).child("pic0.jpg")
let imgTask = dbRef.getData(maxSize: 10*1024*1024, completion: { (data, error) in
if let data = data, let img = UIImage(data: data) {
completion(img)
} else {
completion(nil)
}
})
imgTask.observe(.progress, handler: {(snapshot) in
print (snapshot.progress ?? "NO MORE PROGRESS")
})
imgTask.resume()
}
And now I get
Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)
In function
- (void)invokeFetchCallbacksOnCallbackQueueWithData:(GTM_NULLABLE NSData *)data
error:(GTM_NULLABLE NSError *)error {
// Callbacks will be released in the method stopFetchReleasingCallbacks:
GTMSessionFetcherCompletionHandler handler;
#synchronized(self) {
GTMSessionMonitorSynchronized(self);
handler = _completionHandler;
if (handler) {
[self invokeOnCallbackQueueUnlessStopped:^{
handler(data, error);
// Post a notification, primarily to allow code to collect responses for
// testing.
//
// The observing code is not likely on the fetcher's callback
// queue, so this posts explicitly to the main queue.
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
if (data) {
userInfo[kGTMSessionFetcherCompletionDataKey] = data;
}
if (error) {
userInfo[kGTMSessionFetcherCompletionErrorKey] = error;
}
[self postNotificationOnMainThreadWithName:kGTMSessionFetcherCompletionInvokedNotification
userInfo:userInfo
requireAsync:NO];
}];
}
} // #synchronized(self)
In line handler(data,error);
With error error NSError * domain: #"com.google.HTTPStatus" - code: 404

You can use a completion block to return the UIImage asynchronously.
E.g. you could update your code to the following:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "product_collection_cell", for: indexPath) as! ProductsCollectionViewCell
let prodInCell = searchActive ? filtered[indexPath.row] : products[indexPath.row]
// Set fields
cell.ProductImageView.image = #imageLiteral(resourceName: "DefaultProductImage")
prodInCell.GetProductImage() { image in
cell.ProductImageView.image = image
}
cell.ProductName.text = prodInCell.Name()
cell.ProductPrice.text = String(prodInCell.Price())
cell.productUniqueID = prodInCell.UniqueID()
return cell
}
And:
public func GetProductImage(completion: ((UIImage?) -> Void)) {
let prodID = self.UniqueID()
let dbRef = Storage.storage().reference().child(prodID).child("pic0.jpg")
let imgTask = dbRef.getData(maxSize: 10*1024*1024, completion: { (data, error) in
if let data = data, let img = UIImage(data: data) {
completion(img)
} else {
completion(nil)
}
})
imgTask.observe(.progress, handler: {(snapshot) in
print (snapshot.progress ?? "NO MORE PROGRESS")
})
imgTask.resume()
}

Related

Collectionview in TableviewCell, data repeat

I've a collectionview inside my resizable tablviewCells. Tableview has one cell in each 'n' number of sections. Datasource and delegate of collectionview are set to the tableviewCell. There is an API called on tablview's cellForRowAt, and the result is rendered on the collectionview for each cell. After the result is fetched, a delegate tells the tableview that collectionview is loaded and it should reload that cell without calling the API this time. But the problem is that my collectionview data is repeated after every 2 tableviewCells.
I know prepareForReuse should be override to get rid of cell reuse problems. I've implemented prepareForReuse in my collectionviewCells and set my label.text and imageView.image to nil. However i'm not sure what to add to prepareForReuse for my tableviewCell.
// TableView class
override func viewDidLoad() {
super.viewDidLoad()
storiesSections = [....]
tableView.register(UINib(nibName: "RWFeedTableViewCell", bundle: nil), forCellReuseIdentifier: "reuseIdentifier")
tableView.estimatedRowHeight = 1
tableView.rowHeight = UITableView.automaticDimension
}
override func numberOfSections(in tableView: UITableView) -> Int {
return storiesSections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) as! RWFeedTableViewCell
if cell == nil {
cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "reuseIdentifier") as! RWFeedTableViewCell
}
cell.delegate = self
cell.fetchData(feedSection: storiesSections[indexPath.section], indexPath: indexPath)
return cell
}
// delegate for tableview reload
func collectionViewDidEnd(updatedFeedSection: FeedSection, indexPath: IndexPath) {
storiesSections[indexPath.section] = updatedFeedSection
tableView.beginUpdates()
tableView.endUpdates()
}
// TableViewCell class
override func awakeFromNib() {
super.awakeFromNib()
initializeCode()
}
func initializeCode() {
// Set layout
self.collectionView.collectionViewLayout = RWWaterfallLayout2()
self.collectionView.register(UINib(nibName: "\(ImageThenTitleViewCell.self)", bundle: nil), forCellWithReuseIdentifier: kImageThenTitleCellID)
self.collectionView.register(UINib(nibName: "\(LeftImageCell.self)", bundle: nil), forCellWithReuseIdentifier: kLeftImageCellID)
self.collectionView.contentInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
self.collectionView.isScrollEnabled = false
self.collectionView.dataSource = self
self.collectionView.delegate = self
}
func fetchData(feedSection: FeedSection, indexPath: IndexPath) {
if feedSection.isLoadComplete {
return
}
if let catID = feedSection.categoryID {
let urlString = URL(string: <urlString>)
let urlRequest = URLRequest(url: urlString!)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) { (data, response, error) in
if error == nil {
} else {
print(error?.localizedDescription as Any)
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
do {
guard let todo = try JSONSerialization.jsonObject(with: responseData, options: [])as? [String: Any] else {
print("error trying to convert data to JSON")
return
}
print("success convert data to JSON")
DispatchQueue.main.async {
var updatedFeedSection = feedSection
updatedFeedSection.storiesArray? = (todo["data"]! as! Array)
updatedFeedSection.isLoadComplete = true
self.feedSection = updatedFeedSection
self.collectionView.reloadData()
self.collectionView.performBatchUpdates({
}, completion: { (complete) in
self.collectionViewHeightConstraint.constant = self.collectionView.collectionViewLayout.collectionViewContentSize.height + self.collectionView.contentInset.top + self.collectionView.contentInset.bottom
self.delegate?.collectionViewDidEnd(updatedFeedSection: updatedFeedSection, indexPath: indexPath)
})
}
} catch {
print("error trying to convert data to JSON")
return
}
}
task.resume()
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if self.feedSection == nil {
return 0
} else {
return (self.feedSection?.storiesArray?.count)!
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let indexForTen = indexPath.item%10
let story = self.feedSection?.storiesArray?[indexPath.item]
if indexForTen == 0 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kImageThenTitleCellID, for: indexPath) as! ImageThenTitleViewCell
cell.setupData(story: story!)
return cell
}
else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kLeftImageCellID, for: indexPath) as! LeftImageCell
cell.setupData(story: story!)
return cell
}
}
override func prepareForReuse() {
super.prepareForReuse()
}
// Collectionview Cell
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
func setupData(story: Dictionary<String, Any>){
self.storyImage.image = nil // reset the image
let thumbImage = story["image"] as! Dictionary<String, String>
self.storyTitle.text = story["t"] as? String
self.storyImage.downloaded(from: (thumbImage["m"])!)
self.layer.borderColor = UIColor.lightGray.cgColor
self.layer.borderWidth = 1
self.layer.cornerRadius = 8
}
override func prepareForReuse() {
super.prepareForReuse()
storyImage.image = nil
storyTitle.text = nil
}
// FeedSection struct
struct FeedSection {
var categoryID: String?
var storiesArray : [Dictionary<String, Any>]?
var isLoadComplete: Bool
init(categoryID: String) {
self.categoryID = categoryID
self.storiesArray = []
self.isLoadComplete = false
}
}
Currently the 3rd tableviewCell repeats the data of 1st tablviewCell. How to avoid repeating cell data?
Only problem was the feedSection object in TableViewCell. It should be initialized at the time fetchData() is called. And just reload the collectionView if isLoadComplete is true.
Also since isLoadComplete is set on completion handler of URLSession, I set it to true the time API is called. So the same api will not be called while waiting for response. Maybe an enum could be set for api call and api response events on FeedSection. But for now this works.
func fetchData(feedSection: FeedSection, indexPath: IndexPath) {
self.feedSection = feedSection
if self.feedSection.isLoadComplete {
self.collectionView.reloadData()
return
}
if let catID = feedSection.categoryID {
let urlString = URL(string: <urlString>)
let urlRequest = URLRequest(url: urlString!)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
self.feedSection.isLoadComplete = true
self.delegate?.collectionViewDidEnd(updatedFeedSection: self.feedSection, indexPath: indexPath)
let task = session.dataTask(with: urlRequest) { (data, response, error) in
if error == nil {
} else {
print(error?.localizedDescription as Any)
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
do {
guard let todo = try JSONSerialization.jsonObject(with: responseData, options: [])as? [String: Any] else {
print("error trying to convert data to JSON")
return
}
print("success convert data to JSON")
DispatchQueue.main.async {
self.feedSection.storiesArray? = (todo["data"]! as! Array)
self.feedSection.isLoadComplete = true
self.collectionView.reloadData()
self.collectionView.performBatchUpdates({
}, completion: { (complete) in
self.collectionViewHeightConstraint.constant = self.collectionView.collectionViewLayout.collectionViewContentSize.height + self.collectionView.contentInset.top + self.collectionView.contentInset.bottom
self.delegate?.collectionViewDidEnd(updatedFeedSection: self.feedSection, indexPath: indexPath)
})
}
} catch {
print("error trying to convert data to JSON")
return
}
}
task.resume()
}
}
Set UITableView.reloadData() after get the data.
And check if you set CollectionView.reloadData(), if yes then remove reloadData() of UICollectionView. Only set UITableView.reloadData()

Images not showing in collectionView Swift

In console there is printing of empty imagesArray but I am downloading it in downloadImages function. And in simulator the images won't load
import UIKit
import PinterestLayout
import ProgressHUD
class MainViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var collectionView: UICollectionView!
var postsArray = [Post]()
var imagesArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
if let layout = collectionView.collectionViewLayout as? PinterestLayout {
layout.delegate = self
}
collectionView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
loadItems()
downloadPhoto()
}
func loadItems() {
ref.child("Posts").observe(.childAdded) { (snapshot) in
let snapshotValue = snapshot.value as! Dictionary<String,Any>
if let title = snapshotValue["title"], let price = snapshotValue["price"], let downloadUrl = snapshotValue["downloadUrl"], let category = snapshotValue["category"], let senderUid = snapshotValue["senderUid"] {
let post = Post()
post.title = title as! String
post.price = price as! String
post.downloadUrl = downloadUrl as! String
post.category = category as! String
post.senderUid = senderUid as! String
self.postsArray.append(post)
self.collectionView.reloadData()
}
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tabBarController?.tabBar.shadowImage = UIImage()
tabBarController?.tabBar.backgroundImage = UIImage()
}
func downloadPhoto(){
DispatchQueue.global().async {
self.imagesArray.removeAll() // this is the image array
for i in 0..<self.postsArray.count {
guard let url = URL(string: self.postsArray[i].downloadUrl) else {
continue
}
let group = DispatchGroup()
print(url)
print("-------GROUP ENTER-------")
group.enter()
URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
print(response?.suggestedFilename ?? url.lastPathComponent)
if let imgData = data, let image = UIImage(data: imgData) {
DispatchQueue.main.async() {
self.imagesArray.append(image)
self.collectionView.reloadData()
}
} else if let error = error {
print(error)
}
group.leave()
}).resume()
group.wait()
}
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imagesArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "mainCell", for: indexPath) as! MainCollectionViewCell
cell.imgView.downloadImage(from: self.postsArray[indexPath.row].downloadUrl)
return cell
}
}
extension MainViewController: PinterestLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat {
let image = imagesArray[indexPath.item]
let height = image.size.height
return height
}
}
extension UIImageView {
func downloadImage(from url: String){
let urlRequest = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: urlRequest) {
(data,response,error) in
if error != nil {
print(error ?? error!)
return
}
if let data = data {
DispatchQueue.main.async {
self.image = UIImage(data: data)
}
}
}
task.resume()
}
}
UPDATED CODE:
import UIKit
import PinterestLayout
import ProgressHUD
import Kingfisher
class MainViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var collectionView: UICollectionView!
var postsArray = [Post]()
var imagesArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
if let layout = collectionView.collectionViewLayout as? PinterestLayout {
layout.delegate = self
}
collectionView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
loadItems()
collectionView.reloadData()
}
func loadItems() {
ref.child("Posts").observe(.childAdded) { (snapshot) in
let snapshotValue = snapshot.value as! Dictionary<String,Any>
if let title = snapshotValue["title"], let price = snapshotValue["price"], let downloadUrl = snapshotValue["downloadUrl"], let category = snapshotValue["category"], let senderUid = snapshotValue["senderUid"] {
let post = Post()
post.title = title as! String
post.price = price as! String
post.downloadUrl = downloadUrl as! String
post.category = category as! String
post.senderUid = senderUid as! String
self.postsArray.append(post)
self.downloadPhoto()
print(self.imagesArray.count)
self.collectionView.reloadData()
}
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tabBarController?.tabBar.shadowImage = UIImage()
tabBarController?.tabBar.backgroundImage = UIImage()
}
func downloadPhoto(){
DispatchQueue.global().async {
self.imagesArray.removeAll() // this is the image array
for i in 0..<self.postsArray.count {
guard let url = URL(string: self.postsArray[i].downloadUrl) else {
continue
}
let group = DispatchGroup()
print(url)
print("-------GROUP ENTER-------")
group.enter()
URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
print(response?.suggestedFilename ?? url.lastPathComponent)
if let imgData = data, let image = UIImage(data: imgData) {
DispatchQueue.main.async() {
self.imagesArray.append(image)
let post = Post()
post.image = image
self.collectionView.reloadData()
}
} else if let error = error {
print(error)
}
group.leave()
}).resume()
group.wait()
}
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return postsArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "mainCell", for: indexPath) as! MainCollectionViewCell
let resource = ImageResource(downloadURL: URL(string: postsArray[indexPath.row].downloadUrl)!, cacheKey: postsArray[indexPath.row].downloadUrl)
cell.imgView.kf.setImage(with: resource)
return cell
}
}
extension MainViewController: PinterestLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat {
let image = imagesArray[indexPath.row]
let height = image.size.height / 6
return height
}
}
//AND POST CLASS
import UIKit
class Post {
var title : String = ""
var category : String = ""
var downloadUrl : String = ""
var price : String = ""
var senderUid : String = ""
var image = UIImage()
}
You need to call
downloadPhoto()
inside
loadItems()
as both are asynchronous , here
self.postsArray.append(post)
downloadPhoto()
Note: I recommend having a cache to check before downloading the image or better use SDWebImage
As Sh_Khan said, you need to call downloadPhoto after loadItems has completed, otherwise there wont be any posts to loop over.
Also, a few points to consider here...
You are not reloading any cells once the image download has completed (downloadPhoto)
You are not caching images, so you will end up downloading images often. Once you scroll your collectionView and cells get reused you will download the same images, again.
You are not using DispatchGroup effectively here (in downloadPhoto anyway), you appear to be downloading one image at a time (or trying to), not taking advantage of parallel downloads. If you intend to do this, use a serial queue. But this will slow down loading images considerably.
I prefer to use KingFisher for downloading and caching images, the library already manages most of this for you and leaves you to focus on your app.
If you dont want to use a library, something like this should help...
var imageCache = [String: UIImage]()
func downloadImage(from url: String){
if let image = imageCache[url] as? UIImage {
self.image = image
return
}
let urlRequest = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: urlRequest) {
(data,response,error) in
if error != nil {
print(error ?? error!)
return
}
if let data = data {
DispatchQueue.main.async {
let image = UIImage(data: data)
imageCache[url] = image
self.image = image
}
}
}
task.resume()
}
Kingfisher example:
imageView.kf.setImage(with: url, completionHandler: {
(image, error, cacheType, imageUrl) in
// image: Image? `nil` means failed
// error: NSError? non-`nil` means failed
// cacheType: CacheType
// .none - Just downloaded
// .memory - Got from memory cache
// .disk - Got from disk cache
// imageUrl: URL of the image
})
so have a postsArray and images array
var postsArray = [Post]()
var images = [String: UIImage]() // dictionary, maps url to image
Then when you receive a Post:
let post = Post()
post.title = title as! String
post.price = price as! String
post.downloadUrl = downloadUrl as! String
post.category = category as! String
post.senderUid = senderUid as! String
self.postsArray.append(post)
imageView.kf.setImage(with: url, completionHandler: { (image, error, cacheType, imageUrl) in
// check image is not nil etc
images[url] = image
collectionView.reloadData()
}
CellForRowAt:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "mainCell", for: indexPath) as! MainCollectionViewCell
cell.imgView.image = images[postsArray[indexPath.row].downloadUrl]
return cell
}

Swift Images place in wrong cell and keep chaging in UICollectionview

I have searched but I was not successful to find the answer to my question, I am downloading images from internet and put them into collection view but when I scroll the places are changing even without scrolling they places on the wrong cell here is my code :
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
var Label = cell.viewWithTag(2) as! UILabel
Label.text = namesArray[indexPath.row]
var image = cell.viewWithTag(1) as! UIImageView
let URLString = imgLink[indexPath.row]
let imgUrl = URL(string: URLString)
image.downloadedFrom(url: imgUrl!, contentMode: UIViewContentMode.scaleAspectFit)
UIimg.insert(image.image!, at: indexPath.row)
return cell
}
public 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() { () -> Void in
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)
}
}
UPDATE:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
var image1 = cell.viewWithTag(1) as! UIImageView
let URLString = imgLink[indexPath.row]
let imgUrl = URL(string: URLString)
var Label = cell.viewWithTag(2) as! UILabel
Label.text = namesArray[indexPath.row]
getImage(urlString: URLString) { (success:Bool, data:NSData?, errorDescription:String?) in
if success {
DispatchQueue.main.async() {
let image = UIImage(data: data! as Data)
image1.image = image
}
}
}
return cell
}
func getImage(urlString:String, completionHandler:#escaping (_ success:Bool, _ data:NSData?, _ errorDescription:String?) -> Void) -> Void {
let url = NSURL(string: urlString)
let request = NSMutableURLRequest(url: url! as URL)
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest) { (data, response, error) in
guard let data = data, error == nil else {
completionHandler(false,nil, error?.localizedDescription)
return
}
completionHandler(true, data as NSData?, nil)
}
task.resume()
}
This function is an asynchronous function which will take some time to complete
image.downloadedFrom(url: imgUrl!, contentMode: UIViewContentMode.scaleAspectFit)
So this line..
UIimg.insert(image.image!, at: indexPath.row)
Will run before the above function call has finished downloading the image. This will be causing your issue.
Your downloadedFrom function should use a completion handler to run some code after the image has downloaded for it to work properly.
I usually use a function like the one below for fetching images
func getImage(urlString:String, completionHandler:(success:Bool, data:NSData?, errorDescription:String?) -> Void) -> Void {
let url = NSURL(string: urlString)
let request = NSMutableURLRequest(URL: url!)
let task = session.dataTaskWithRequest(request) { (data, response, error) in
guard let data = data where error == nil else {
completionHandler(success: false,data: nil, errorDescription: error?.localizedDescription)
return
}
completionHandler(success: true, data: data, errorDescription: nil)
}
task.resume()
}
Which can be used in a tableCell/collectionCell like this:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let photo = fetchedResultsController.objectAtIndexPath(indexPath) as! Photo
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! AlbumCell
cell.backgroundColor = UIColor.grayColor()
cell.imageView.image = UIImage(named: "placeholder")
if let image = photo.image {
cell.imageView.image = image
} else {
VTClient.sharedInstance().getImage(photo.url) { (success:Bool, data:NSData?, errorDescription:String?) in
if success {
dispatch_async(dispatch_get_main_queue()) {
let image = UIImage(data: data!)
cell.imageView.image = image
FlickrClient.Caches.imageCache.storeImage(image, withIdentifier: photo.id)
}
}
}
}
return cell
}
}
See my project here https://github.com/martinjkelly/virtual-tourist/blob/master/Virtual Tourist for more information on how this is used

TableView Cell show image with dispatch_async shows "unexpected non-void return value in void function" issue

I want to show the image in the TableViewCell. There are the codes:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "myCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! DIYSquareALLCell
cell.titles!.text = titles[indexPath.row]
cell.leftImages!.image = getPic(leftImages[indexPath.row])
return cell
}
func getPic(PicURL: String) -> UIImage! {
let image = self.imageCache[PicURL] as UIImage?
if image == nil {
let url = NSURL(string: PicURL.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!)
if let data = NSData(contentsOfURL: url!) {
imageCache[PicURL] = UIImage(data: data)
return UIImage(data: data)!
}
} else {
return image
}
return nil
}
But scrolling the TableView is very lag so I change the function and add some dispatch_async feature in it.
It shows the issue "unexpected non-void return value in void function" in my getPic function.
After I changed, there are the codes:
func getPic(PicURL: String) -> UIImage! {
let image = self.imageCache[PicURL] as UIImage?
if image == nil {
let url = NSURL(string: PicURL.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!)
let priority = DISPATCH_QUEUE_PRIORITY_HIGH
dispatch_async(dispatch_get_global_queue(priority, 0)) {
let data = NSData(contentsOfURL: url!)
if data != nil {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.imageCache[PicURL] = UIImage(data: data!)
return UIImage(data: data!)// here is the issue
})
}
}
} else {
return image
}
return nil
}
Anyone can tell me how to fix it? Thanks!
You can't return a value r an object when using the Asynchronous task, The function which is running on the main thread and it won't wait for your async task to be finish.
Lets do this with the Closure.
Your code should be like this:
typealias CompletionHandler = (image: UIImage) -> Void
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: testCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! testCell
downloadFileFromURL(NSURL(string: "http://img.youtube.com/vi/PCwL3-hkKrg/sddefault.jpg")!, completionHandler:{(img) in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.imgView.image = img
})
})
return cell
}
func downloadFileFromURL(url1: NSURL?,completionHandler: CompletionHandler) {
// download code.
if let url = url1{
let priority = DISPATCH_QUEUE_PRIORITY_HIGH
dispatch_async(dispatch_get_global_queue(priority, 0)) {
let data = NSData(contentsOfURL: url)
if data != nil {
print("image downloaded")
completionHandler(image: UIImage(data: data!)!)
}
}
}
}
Sample project uploaded to GIT.

call value variable from outside function in swift

I have a problems to call a variable in class, from outside function.
Swift gives me the following error: Use of unresolved identifier 'imageFilename'
How I can solve it? How should I get the value of the Filename variable?
My code:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if (collectionView == self.collectionView1)
{
let cell : FeaturedCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier(self.reuseIdentifierFeatured, forIndexPath: indexPath) as! FeaturedCollectionViewCell
let imgURL: NSURL = NSURL(string: "http://localhost:9001/feature-0.jpg")!
let request: NSURLRequest = NSURLRequest(URL: imgURL)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request){
(data, response, error) -> Void in
if (error == nil && data != nil)
{
func display_image()
{
let imageFilename = UIImage(data: data!)
}
dispatch_async(dispatch_get_main_queue(), display_image)
}
}
task.resume()
cell.featuredImage.image = UIImage(named: imageFilename)
return cell
}
}
Image capture link
How about if you declare the variable outside of the function and inside of the function you set the value. Then you have access to the variable and its value.
Your Problem is definetly that you can not access the variable, because it is just know inside of the function.
Code:
try it like this...
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if (collectionView == self.collectionView1)
{
let cell : FeaturedCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier(self.reuseIdentifierFeatured, forIndexPath: indexPath) as! FeaturedCollectionViewCell
var imageFilename: UIImage
let imgURL: NSURL = NSURL(string: "http://localhost:9001/feature-0.jpg")!
let request: NSURLRequest = NSURLRequest(URL: imgURL)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request){
(data, response, error) -> Void in
if (error == nil && data != nil)
{
func display_image()
{
imageFilename = UIImage(data: data!)
}
dispatch_async(dispatch_get_main_queue(), display_image)
}
}
task.resume()
cell.featuredImage.image = UIImage(named: imageFilename)
return cell
}
}
Write me if this worked for you.
The scope of imageFileName is the function display_image in which it is declared, it is not visible outside that if. The problem is not the access of a variable in a class, your custom cell class doesn't seem to declare a variable named imageFileName
Edit
Why don't you set the image inside the completion closure?:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if (collectionView == self.collectionView1) {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(self.reuseIdentifierFeatured, forIndexPath: indexPath) as! FeaturedCollectionViewCell
let imgURL: NSURL = NSURL(string: "http://localhost:9001/feature-0.jpg")!
let request: NSURLRequest = NSURLRequest(URL: imgURL)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) {
(data, response, error) -> Void in
if (error == nil && data != nil) {
dispatch_async(dispatch_get_main_queue()) {
cell.featuredImage.image = UIImage(data: data!)
}
}
}
task.resume()
return cell
}
}
Be aware that due to the fact that asynchronous request may complete in an undefined order and cell reuse, you can end up with incorrect images for cells, you could save the image url in the cell and check if it is the same as the one captured in the closure when the the closure completes.