How can I check when all my images have downloaded? - ios

I am downloading a couple of images and cache them. Then I am recalling the cached images and add them as a subView to my scrollView (which is horizontal using a page controller). I want to do both of that when view appears without any other actions required. Unfortunately, it already adds the subview before the images were cached, because it goes through the code while the images are getting downloaded. It follows that nothing is added as a subView.
It works when I download and cache the images when the view appears and I add them as a subView when I press a button, but I would like for that to work automatically.
I am not sure if my code helps for a better understanding but here it is.
This is my code to download the images
func downloadImage(url: URL) {
let dataTask = URLSession.shared.dataTask(with: url) { data, responseURL, error in
var downloadedImage:UIImage?
if let data = data {
downloadedImage = UIImage(data: data)
}
// if download actaully got an image
if downloadedImage != nil {
self.sum += 1
self.cache.setObject(downloadedImage!, forKey: url.absoluteString as NSString) // cache the image
// add the url as value to the dictionary
self.imageURLS[self.sum] = url.absoluteString
}
}
dataTask.resume()
}
And this is what I use to add the subviews
func appendImages(){
// sort the values of the dictionary by the greatest
let sortedImageURLs = Array(imageURLS.keys).sorted(by: >)
var stringURLs = [String]() // empty string array to append the URL's
// for loop which goes through all integers in "creation Date" array
for keys in sortedImageURLs {
let url: String? = imageURLS[keys] // url which is connected to the key
stringURLs.append(url!) // append the url to the url array
}
// -> the url string array starts with the latest loaded url
var originSubview = 0 // counter of the origin of the subviews
for urls in stringURLs {
// 1.
frame.origin.x = scrollView.frame.size.width * CGFloat(originSubview)
frame.size = scrollView.frame.size
// 2.
let imageView = UIImageView(frame: frame)
// get the image which is cahed under the url
let image = cache.object(forKey: urls as NSString)
// set the image of image view as the cached image
imageView.image = image
// and add that image view as a subview to the scrollView
scrollView.addSubview(imageView)
// increase counter variable by one
//-> next images origin is at the end of the image before
originSubview += 1
}
// 3.
scrollView.contentSize = CGSize(width: ((scrollView.frame.size.width) * CGFloat(specialsCounter)), height: (scrollView.frame.size.height))
scrollView.delegate = self
pageControl.numberOfPages = specialsCounter
}

You can use a DispatchGroup to notify when all your downloads have completed.
Basically you enter() before you start a download and leave when the download has finished. notify will trigger when all entered tasks has left
Here is an example on how to do it. I made a completion block for your downloadImage function.
let dispatchGroup = DispatchGroup()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
for url in imageUrls {
self.dispatchGroup.enter()
downloadImage(url: url) {
self.dispatchGroup.leave()
}
}
self.dispatchGroup.notify(queue: .main) {
//All images has been downloaded here
}
}
downloadImage function with completion:
func downloadImage(url: URL, completion: () -> ()) {
let dataTask = URLSession.shared.dataTask(with: url) { data, responseURL, error in
var downloadedImage:UIImage?
if let data = data {
downloadedImage = UIImage(data: data)
}
// if download actaully got an image
if downloadedImage != nil {
self.sum += 1
self.cache.setObject(downloadedImage!, forKey: url.absoluteString as NSString) // cache the image
// add the url as value to the dictionary
self.imageURLS[self.sum] = url.absoluteString
completion()
} else {
completion()
}
}
dataTask.resume()
}

Related

How do I know in swift 5 if the image was actually downloaded from an url?

I am using a page control with a scroll view and want to add downloaded images to the scroll view until the provided link does not download an image anymore. Right now, the link downloads an image until the index is not greater than 7 but the loop still goes on. Apparently it does not return an error, when the url can be still loaded although there is no image to download. This is what I get when I paste the url manually in a browser with an index greater than 7.
This is the code I used. I would like to know if there is a way to test if actual an image was downloaded.
var test = 15
func setupScreens() {
for index in 1..<test {
// 1.
frame.origin.x = scrollView.frame.size.width * CGFloat(index - 1)
frame.size = scrollView.frame.size
// 2.
let imageView = UIImageView(frame: frame)
let urlSession = URLSession(configuration: .default)
let url = URL(string: "https://jarisstoriesphotographyphoto.files.wordpress.com/2020/06/menu\(index).png")!
// Create Data Task
let dataTask = urlSession.dataTask(with: url) { [weak self] (data, _, error) in
if let error = error {
print(error)
self?.test = 0 // should end the loop
}
if let data = data {
DispatchQueue.main.async {
// Create Image and Update Image View
imageView.image = UIImage(data: data)
self?.scrollView.addSubview(imageView)
}
}
}
// Start Data Task
dataTask.resume()
}
You can simply check like this before setting the image in your code:
DispatchQueue.main.async {
if let image = UIImage(data: data) { //Image is available in the url
imageView.image = image
self?.scrollView.addSubview(imageView)
}else {
print("Image not available in this url")
}
}
Keep Coding........... :)

iOS: Add UIProgressView inside uicollectionview to show upload percentage of uploading to firebase storage

I'm develop an iOS app that upload a list of images to firebase storage, I need to put a progress view inside each cell of uicollectionview (which show each image in datasource) to indicate percentage of uploading this image to firebase storage.
I try this code:
func Upload(imageView: UIImageView)
{
// Data in memory
var data = Data()
data = UIImageJPEGRepresentation(self.pickedImage, 0.8)!
// Create a root reference
let storageRef = Storage.storage().reference()
let imageName = NSUUID().uuidString
// Create a reference to the file you want to upload
let imageRef = storageRef.child("images/\(imageName).jpg")
// Upload the file to the path "images/rivers.jpg"
let uploadTask = imageRef.putData(data, metadata: nil) { (metadata, error) in
guard let metadata = metadata else {
// Uh-oh, an error occurred!
return
}
}
let observer = uploadTask.observe(.progress) { snapshot in
print("snapshot.progress",snapshot.progress)
}
uploadTask.observe(.progress) { snapshot in
let percentComplete = 100.0 * Double(snapshot.progress!.completedUnitCount)
/ Double(snapshot.progress!.totalUnitCount)
print("percentComplete",percentComplete)
let placeholderImage = UIImage(named: "la")
imageView.sd_addActivityIndicator()
imageView.sd_showActivityIndicatorView()
imageView.sd_setImage(with: imageRef, placeholderImage: placeholderImage)
}
uploadTask.observe(.success) { snapshot in
self.url = imageName
print(imageName)
print("success")
imageView.sd_removeActivityIndicator()
}
uploadTask.observe(.failure) { snapshot in
print("Failuer")
}
}
and:
arrayOfImages?[(arrayOfImages?.count)! - 1]?.Upload(imageView: cell.workImage)
but I'm couldn't to get that because reuse of cell in UICollectionView, How can I get that?
1- Create a class and paste this code inside a function
2- Make progress property inside that class and regularly update it inside the progress completion and another property for the image
3- Create an instance array inside the class and init it with the image and start the upload
4- Inside cellForRowAt get the current progress value for every index , that's how you avoid cell reusing and uploading of image that be uploaded or currently uploading
//
It would be something like this
class Upload {
var currentState:State = .none
var progress:Float = 0
var img:UIImage
init(image:UIImage) {
img = image
}
func start () {
currentState = .uploading
///
let observer = uploadTask.observe(.progress) { snapshot in
self.progress = snapshot.progress
}
///
uploadTask.observe(.success) { snapshot in
currentState = .uploaded
}
//
}
}
enum State {
case uploading,uploaded,none
}
//
var arr = [Upload]()
for i in 0..<numberOfImages {
let img = UIImage(named: "\(i).png") // or get it anyWay
let up = Upload(image: img)
up.start() // start it in cellForRowAt if item state is none
arr.append(up)
}

Swift Grand Central Dispatch Queues and UIImages

I know this type of question has been asked 1e7 times but I have come across a specific issue that I don't think has been covered/is blatantly obvious but I am too novice to fix it on my own.
I have the following code snippet within my cellForRowAt method in a TableViewController:
let currentDictionary = parser.parsedData[indexPath.row] as Dictionary<String,String>
let urlString = currentDictionary["media:content"]
if urlString != nil {
let url = NSURL(string: urlString!)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url! as URL) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
DispatchQueue.main.async {
cell.thumbnailImageView.image = UIImage(data: data!)
}
}
}
Which executes fine, downloads the images and assigns them to the UIImageView of each tableViewCell.
There is a finite delay when scrolling the table as the images are downloaded 'on the fly' so to speak.
What I want to do is pre-download all these images and save them in a data structure so they are fetched from URL's less frequently.
I have tried the following implementation:
var thumbnail = UIImage()
for item in parser.parsedData {
let currentDictionary = item as Dictionary<String,String>
let title = currentDictionary["title"]
let link = currentDictionary["link"]
let urlString = currentDictionary["media:content"]
let url = NSURL(string: urlString!)
if urlString != nil {
let url = NSURL(string: urlString!)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url! as URL)
DispatchQueue.main.sync {
thumbnail = UIImage(data: data!)!
}
}
}
var newsArticle: News!
newsArticle = News(title: title!, link: link!, thumbnail: thumbnail)
news.append(newsArticle)
Where news is my data structure. This code also executes fine, however each thumbnail is a 0x0 sized image, size {0, 0} orientation 0 scale 1.000000, according to the console output.
Does anyone have any ideas how to download these images but not immediately assign them to a UIImageView, rather store them for later use?
The problem is that you create your newsArticle before the global dispatch queue even started to process your url. Therefore, thumbnail is still the empty UIImage() created in the very first line.
You'll have to create the thumbnail inside the inner dispatch closure, like:
for item in parser.parsedData {
guard let currentDictionary = item as? Dictionary<String,String> else { continue /* or some error handling */ }
guard let title = currentDictionary["title"] else { continue /* or some error handling */ }
guard let link = currentDictionary["link"] else { continue /* or some error handling */ }
guard let urlString = currentDictionary["media:content"] else { continue /* or some error handling */ }
guard let url = URL(string: urlString) else { continue /* or some error handling */ }
DispatchQueue.global().async {
if let data = try? Data(contentsOf: url) {
DispatchQueue.main.sync {
if let thumbnail = UIImage(data: data) {
let newsArticle = News(title: title, link: link, thumbnail: thumbnail)
news.append(newsArticle)
}
}
}
}
}
By the way, your very first code (cellForRow...) is also broken: You must not reference the cell inside the dispatch closure:
DispatchQueue.main.async {
// Never do this
cell.thumbnailImageView.image = UIImage(data: data!)
}
Instead, reference the IndexPath, retrieve the cell inside the clousure, and go on with that cell. But as you already mentioned, there are many many entries on stackoverflow regarding this issue.

Add an image as an accessory in your UITableView in Swift 3

In my project, I show a UITableView, which currently has text describing a show's name and genre loading from a remote JSON file.
That all works. What I want next is to use the URL from the JSON file and load a thumbnail next to each show.
Using a tutorial, I have added a function to download the remote image with a print to test if it's successful.
if let shows_list = json as? NSArray
{
for i in 0 ..< data_list.count
{
if let shows_obj = shows_list[i] as? NSDictionary
{
let show_name = shows_obj["show"] as? String
let show_genre = shows_obj["genre"] as? String
let show_image = shows_obj["thumbnail"] as? String
TableData.append(show_name! + " | " + show_genre!)
let testPictureURL = URL(string: show_image!)!
let session = URLSession(configuration: .default)
// Here's the download task where I'm grabbing the image
let downloadPicTask = session.dataTask(with: testPictureURL) { (data, response, error) in
// The download has finished.
if let e = error {
print("Error downloading cat picture: \(e)")
} else {
// No errors found.
if let res = response as? HTTPURLResponse {
print("Downloaded picture with response code \(res.statusCode)")
if let imageData = data {
// Now I know I have data, so I think I can use UIImage to convert it into an image
let image = UIImage(data: imageData)
} else {
print("Couldn't get image: Image is nil")
}
} else {
print("Couldn't get response code for some reason")
}
}
}
downloadPicTask.resume()
}
There are three items in the JSON array, and I get three printed statements that the picture was download: but the image does not appear.
My theory: since this is a table, maybe I have to add this as an accessory, but there isn't an image accessory subclass.
I am new to Swift -- do you have any ideas about how I should append this uploaded image to the table.
This is probably being caused by the asynchronous behavior of URLSession so when the requested image returns the view is already loaded.
To solve that, you can use a callback, for instance:
func myFunction(completion: (returnedImage -> UIIMage) -> Void){
//...
let downloadPicTask = session.dataTask(with: testPictureURL) { (data, response, error) in
//...
let image = UIImage(data: imageData)
completion(returnedImage: image)
//...
}
downloadPicTask.resume()
}
}
By using a callback, let's say that you have a method called myFunction(completion:), so now when you call the method you can handle whatever comes back from completion:
myFunction { (image) in
DispatchQueue.main.async { cell.imageView.image = image }
}

IOS tableview reusing old images while downloading new images issue

I have read a similar post in Reusable cell old image showing and I am still getting the same issues. Essentially I have a TableView that downloads images from amazon s3 as you scroll down everything works good until you get to about the 12th or 13th image. What happens is that the image in the row before shows up in the new row for about 2 seconds while the new image is being downloaded . This is my code ( I'm still new at swift and learning IOS) . The stream_image_string as the full URL to download the images and PicHeight is an integer saved with the image height since every image usually has a different height .
var Stream_Cache = NSCache<AnyObject, AnyObject>()
var stream_image_string = [String]()
var PicHeight = [Int]()
This below is inside the UITableViewCell, first I check if there is a url which it will contain more than 0 characters . I then check if the image/url is saved in the Cache if not then I download it .
if stream_image_string[indexPath.row].characters.count > 0 {
if let image = Stream_Cache.object(forKey: stream_image_string[indexPath.row] as AnyObject) as? UIImage {
DispatchQueue.main.async(execute: { () -> Void in
cell.stream_image.image = image
})
} else {
if cell.stream_image != nil {
let strCellImageURL = self.stream_image_string[indexPath.row]
let imgURL: NSURL = NSURL(string: strCellImageURL)!
let request:NSURLRequest = NSURLRequest(url: imgURL as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
cell.Stream_Image_Height.constant = CGFloat(Float(cell.pic_height!))
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
DispatchQueue.main.async(execute: { () -> Void in
if data != nil {
cell.stream_image.image = UIImage(data: data!)
} else {
cell.Stream_Image_Height.constant = 0
cell.stream_image.image = nil
}
})
});
task.resume()
}
}
} else {
cell.Stream_Image_Height.constant = 0
}
In my UITableViewCell file I set the image to a default image in case it wasn't done loading the new image but it hasn't worked
class HomePageTVC: UITableViewCell {
#IBOutlet weak var stream_image: UIImageView!
var pic_height: Int?
override func awakeFromNib() {
super.awakeFromNib()
stream_image.image = #imageLiteral(resourceName: "defaultImage")
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
any suggestions would be great
You're facing a pretty common cell reuse issue. When you dequeue a cell that was used before, it may already have an image installed in its image view. Set the image to nil before beginning an async download:
if let imageString = stream_image_string[indexPath.row].characters,
!imageString.isEmpty {
if let image = Stream_Cache.object(forKey: imageString) as? UIImage {
cell.stream_image.image = image
} else {
//Clear out any old image left in the recycled image view.
cell.stream_image.image = nil
//Your code to download the image goes here
}
}
Note that there's no need to wrap the cell.stream_image.image = image code in a call to DispatchQueue.main.async(). That code will be run on the main thread.
You do, however, need the second DispatchQueue.main.async() wrapper around the code inside the dataTask's completionHandler, since URLSession's completion handers are called on a background queue by default.

Resources