How to download images async for WidgetKit - ios

I am developing Widgets for iOS and I really don't know how to download images for the widgets.
The widget currently downloads an array of Objects, and every object has a URL of an image. The idea is that every object makes a SimpleEntry for the Timeline.
What's the best way of achieving this? I read that Widgets shouldn't use the ObservableObject. I fetch the set of objects in the timeline provider, which seems to be what Apple recommends. But do I also download the images there and I wait until all are done to send the timeline?
Any advice would be very helpful,

Yes, you should download the images in the timeline provider and send the timeline when they are all done. Refer to the following recommendation by an Apple frameworks engineer.
I use a dispatch group to achieve this.
Something like:
let imageRequestGroup = DispatchGroup()
var images: [UIImage] = []
for imageUrl in imageUrls {
imageRequestGroup.enter()
yourAsyncUIImageProvider.getImage(fromUrl: imageUrl) { image in
images.append(image)
imageRequestGroup.leave()
}
}
imageRequestGroup.notify(queue: .main) {
completion(images)
}
I then use SwiftUI's Image(uiImage:) initializer to display the images

I dont have a good solution, but I try to use WidgetCenter.shared.reloadAllTimelines(), and it make sence.
In the following code.
var downloadImage: UIImage?
func downloadImage(url: URL) -> UIImage {
var picImage: UIImage!
if self.downloadImage == nil {
picImage = UIImage(named: "Default Image")
DispatchQueue.global(qos: .background).async {
do {
let data = try Data(contentsOf: url)
DispatchQueue.main.async {
self.downloadImage = UIImage.init(data: data)
if self.downloadImage != nil {
DispatchQueue.main.async {
WidgetCenter.shared.reloadAllTimelines()
}
}
}
} catch { }
}
} else {
picImage = self.downloadImage
}
return picImage
}
Also you have to consider when to delete this picture.
This like tableView.reloadData().

Related

Firebase Storage image download extremely slow

I am developing an IOS app in which I have posts that get fetched from the firestore database. Each post contains references to the firebase storage, where the corresponding images are stored. When I want to download the images, it takes ages for them to be downloaded, around 10-15 seconds. I load them asynchronous. I tried downloading them via
the firebase SDK getData() method
downloading the url and then downloading the content behind the URL
downloading them via passing the url into an asyncImageView
However, none of these methods achieve any good results that could be used for a decent UX. How can I make this faster?
Previous answers suggested making the storage public... Isn't displaying them via the URL a public method?
If it is not and I have to make it public, how can I prevent that everybody can see every image, whether it is a user of the app or not. Is it possible to have a "public" storage but still not making it accessible for everyone?
Should I change to a different provider?
Code:
func orderedImageDownload3(imageRefs: [String], doc: QueryDocumentSnapshot){
let group = DispatchGroup()
var images = [UIImage]()
let storageRef = Storage.storage().reference()
for ref in imageRefs {
let fileRef = storageRef.child(ref)
group.enter()
fileRef.downloadURL { url, error in
if let error = error {
// Handle any errors
print(error)
} else {
//Do the download
if let url = url {
self.getImage(from: url) {data, response, error in
guard let data = data, error == nil else { return }
print(response?.suggestedFilename ?? url.lastPathComponent)
print("Download Finished")
// always update the UI from the main thread
if let image = UIImage(data: data){
images.append(image)
group.leave()
}
}
}
}
}
}
group.notify(queue: .main) {
//put images into observable object
}
}
func getImage(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
}

SDWebImage vs. Nuke for Swift

I have been using Nuke up until last week but I switched to SDWebImage because it is a few times more popular than Nuke and seems more built out.
However, after switching to SDWebImage I have noticed significantly longer build times. I am not asking for an opinion here. In terms of tangible metrics, is Nuke more performant and lighter than SDWebImage?
Although I do not know SDWebImage inside and out, it seems SDWebImage has tons of feature but lacks a key one: I have not found how to set a failure image in case of network failure while loading image. Although, it is extremely easy to do in Nuke.
let options = ImageLoadingOptions(
failureImage: #imageLiteral(resourceName: "image-not-found")
)
Nuke.loadImage(with: URL(string: products[indexPath.row].imageLink)!, options: options, into: cell.productImage)
It's not neccesary/recommended to use external frameworks just for loading an image from an URL.
If you use an external framework you are adding unnecesary codes to your project because there is no framework that just add a function to load an image, it's probably that you are adding more unnecesary stuff with that framework.
I recommend you to implement it by yourself! It going to be more performant and easy than adding an external framework.
This is a code to load an image without external frameworks:
import UIKit
typealias GMImageServiceSuccess = (UIImage) -> Void
typealias GMImageServiceFail = (Error) -> Void
class GMImageService {
// MARK: - Class vars.
private static let imageServiceCache = NSCache<NSString, UIImage>()
// MARK: - Vars.
private var currentDataTask: URLSessionDataTask?
// MARK: - Fetch images functions.
func gmImageFromURL(_ urlString: String, sucess: GMImageServiceSuccess?, fail: GMImageServiceFail?) {
if let imageFromCache = GMImageService.imageServiceCache.object(forKey: urlString as NSString) {
sucess?(imageFromCache)
return
}
guard let imageURL = URL(string: urlString) else {
// ERROR.
return
}
URLSession.shared.dataTask(with: imageURL) { data, response, error in
guard let imageData = data else {
// ERROR.
return
}
if let imageToCache = UIImage(data: imageData) {
DispatchQueue.main.async {
GMImageService.imageServiceCache.setObject(imageToCache, forKey: urlString as NSString)
sucess?(imageToCache)
}
} else {
// ERROR.
}
}.resume()
}
// MARK: - Cancel images functions.
func mpImageCancelCurrent() {
if let currentTask = self.currentDataTask {
currentTask.cancel()
}
}
}
Let me know if you need to implement an image cache, it's easy in swift!

Images not going straight into array

I have a small problem with some code here. I am trying to populate a collection view with Five Names, descriptions and Images.
I am able to successfully to download all of the above into their respected arrays.
The problem is that the first time I perform the segue the image array has zero values in it. Then I go back a page and re-enter the page to find that all of the arrays have been populated successfully....
This is really annoying. Here is my code:
//arrays of names, descriptions and images
var names:[String] = []
var descriptions: [String] = []
var imagesArray: [UIImage] = []
Heres where I get the images:
func downloadImages(){
for x in 1...5{
let url = URL(string: "https://www.imagesLocation.com/(x).png")
let task = URLSession.shared.dataTask(with: url!){(data, response, error) in
guard
let data = data,
let newImage = UIImage(data: data)
else{
print("Could not load image from URL: ",url!)
return
}
DispatchQueue.main.async {
self.imagesArray.append(newImage)
}
}
task.resume()
}
loadDataFromFirebase()
}
Heres where I download the Names and Descriptions from:
func loadDataFromFirebase() {
// Fetch and convert data
let db = Firestore.firestore()
db.collection(self.shopName).getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
return
} else {
for document in snapshot!.documents {
let name = document.get("Name") as! String
let description = document.get("Description") as! String
self.names.append(name)
self.descriptions.append(description)
}
self.setupImages() //safe to do this here as the firebase data is valid
}
}
}
Heres where I setup the collection view with the Names, Description and Images array contents:
func setupImages(){
do {
if imagesArray.count < 5 || names.count < 5 || descriptions.count < 5 {
throw MyError.FoundNil("Something hasnt loaded")
}
self.pages = [
Page(imageName: imagesArray[0], headerText: names[0], bodyText: descriptions[0]),
Page(imageName: imagesArray[1], headerText: names[1], bodyText: descriptions[1]),
Page(imageName: imagesArray[2], headerText: names[2], bodyText: descriptions[2]),
Page(imageName: imagesArray[3], headerText: names[3], bodyText: descriptions[3]),
Page(imageName: imagesArray[4], headerText: names[4], bodyText: descriptions[4]),
]
}
catch {
print("Unexpected error: \(error).")
}
}
As you can see from the image below, every array is populating successfully apart from the images array:
Here is the segue from the previous page's code:
DispatchQueue.main.async(){
self.performSegue(withIdentifier: "goToNext", sender: self)
}
Any help is welcome :)
Your question is just a variant of the classic, "Why is my asynchronous function returning empty data?" I've answered a couple of these questions, and I'll include an analogy that explains the issue. You might understand the issue already, but I'll include it anyway for future readers:
Your mom is cooking dinner and asks you to go buy a lemon.
She starts cooking, but she has no lemon!
Why? Because you haven't yet returned from the supermarket, and your
mom didn't wait.
Source
The main issue here is that you are calling loadDataFromFirebase way too early. You assume that it will execute only after your URL requests have completed, but that is not the case. Why? Because the URL requests are executed asynchronously. That is, they run on another thread instead of blocking the thread that calls dataTask.resume. This is why, as Shashank Mishra suggests, you should use a DispatchGroup. Additionally, there is no guarantee that your images will load in the order that you begin the data tasks. I have included a fix below.
Generally, I would recommend defining variables strictly in the scopes in which you need them. Keeping names, descriptions, and images at such a high scope makes it too easy to make mistakes. I suggest refactoring your functions and deleting those three class-level arrays. Instead:
func loadDataFromFirebase(images: [UIImage]) {
// same function as you posted, except make names and descriptions local variables and
// replace self.setupImages() with:
DispatchQueue.main.async {
self.setupImages(images: images, names: names, descriptions: descriptions)
}
}
func setupImages(images: [UIImage], names: [String], descriptions: [String]) {
guard images.count == 5, names.count == 5, descriptions.count == 5 else {
print("Missing data.")
return
}
self.pages = (0..<5).map({ Page(image: images[$0], header: names[$0], body: descriptions[$0]) })
// super important!!!
tableView.reloadData()
}
Finally, here is my suggestion for a thread-safe downloadImages function:
func downloadImages() {
var images = [UIImage?](repeating: nil, count: 5)
let dispatchGroup = DispatchGroup()
for i in 1...5 {
dispatchGroup.enter()
let url = URL(string: "https://www.imagesLocation.com/\(i).png")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data, let image = UIImage(data: data) else {
print("Could not load image from", url)
dispatchGroup.leave()
return
}
images[i] = image
dispatchGroup.leave()
}.resume()
}
dispatchGroup.notify(queue: .main) {
guard images.allSatisfy({$0 != nil}) else {
print("Failed to fetch all images.")
return
}
self.loadDataFromFirebase(images: images.compactMap({$0}))
}
}
As Fattie pointed out, you should use addSnapshotListener rather than getDocuments. Also, you should add the listener/get documents while downloading the images instead of after, which will be faster. However, I am not adding either to my answer because this is already quite long, and if you have trouble with it you can post another question.
You can use DispatchGroup to achieve asynchronous calls -
func downloadImages() {
let dispatchGroup = DispatchGroup()
for x in 1...5 {
dispatchGroup.enter()
let url = URL(string: "https://www.imagesLocation.com/(x).png")
let task = URLSession.shared.dataTask(with: url!){(data, response, error) in
guard
let data = data,
let newImage = UIImage(data: data)
else{
print("Could not load image from URL: ",url!)
dispatchGroup.leave()
return
}
self.imagesArray.append(newImage)
dispatchGroup.leave()
}
task.resume()
}
dispatchGroup.notify(queue: DispatchQueue.main) {
self.loadDataFromFirebase()
}
}
Call "loadDataFromFirebase()" method on getting all 5 responses as above. It will always have all images before loading it on view.
You're misunderstanding how Firebase works.
Essentially.
Don't use getDocuments. Use .addSnapshotListener
and
Basically each time the snapshot arrives, simply call .reloadData() on the table.
A full tutorial is beyond the scope of an answer here but there are many, many, tutorials around.
Just a typical fragment ...
let db = Firestore.firestore().db.collection("yourCollection")
.whereField("user", isEqualTo: uid)
.addSnapshotListener { [weak self] documentSnapshot, error in
guard let self = self else { return }
guard let ds = documentSnapshot else {
return print("error: \(error!)")
}
self.displayItems = .. that data
self.tableView.reloadData()
}
Note the .reloadData()
Also ..
It's true that you can store an image (binary data) right in Firestore.
But really never, ever, do that - it's completely useless.
Simply use the dead-easy Firebase/Storage system where you can host images for free. Then they have completely normal URLs and so on.
Full tutorial: https://stackoverflow.com/a/62626214/294884

Showing image from URL in iOS app [duplicate]

This question already has answers here:
Loading/Downloading image from URL on Swift
(39 answers)
Closed 5 years ago.
EDIT 3: Please also read my comment in the "answered" tagged answer. I think I won't use my synchronous method but change to the suggested asynchronous methods that were also given!
Ok I am struggling with some basic concepts of showing images from an URL from the internet on my app.
I use this code to show my image on an UIIamgeView in my ViewController:
func showImage() {
let myUrlImage = URL(string: linkToTheImage)
let image = try? Data(contentsOf: myUrlImage!)
imageView1.image = UIImage(data: image!)
}
Now basically I have the following question:
Is the whole image downloaded in this process?
Or works the UIImageView like a "browser" in this case and doesn't download the whole picture but only "positions" the image from the URL into my UIImageView?
EDIT:
The reason I asked is, I am basically doing a quiz app and all I need in the view is an image from a URL for each question - so it's no difference if I do it asynchronous or synchronous because the user has to wait for the image anyways. I am more interested in how do I get the fastest result:
So I wanted to know if my code really downloads the picture as a whole from the URL or just "Positions" it into the UIImageView?
If in my code the picture is downloaded in its full resolution anyways, then you are right, I could download 10 pictures asynchronously when the player starts the quiz, so he hopefully doesn't have to wait after each answer as long as he would wait when I start downloading synchronously after each answer.
Edit 2:
Because my Question was tagged as similar to another some more explanation:
I already read about synchronous and asynchronous downloads, and I am aware of the downsides of synchronous loading.
I am more interested in a really basic question, and I get the feeling I had one basic thing really wrong:
My initial thought was that if I open a link in my browser, for example this one,
https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/68dd54ca-60cf-4ef7-898b-26d7cbe48ec7/10-dithering-opt.jpg
the browser doesn't download the whole picture. But I guess this isn't the case? The whole picture is downloaded?
Never use Data(contentsOf:) to display data from a remote URL. That initializer of Data is synchronous and is only meant to load local URLs into your app, not remote ones. Use URLSession.dataTask to download image data, just as you would with any other network request.
You can use below code to download an image from a remote URL asynchronously.
extension UIImage {
static func downloadFromRemoteURL(_ url: URL, completion: #escaping (UIImage?,Error?)->()) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil, let image = UIImage(data: data) else {
DispatchQueue.main.async{
completion(nil,error)
}
return
}
DispatchQueue.main.async() {
completion(image,nil)
}
}.resume()
}
}
Display the image in a UIImageView:
UIImage.downloadFromRemoteURL(yourURL, completion: { image, error in
guard let image = image, error == nil else { print(error);return }
imageView1.image = image
})
You can do it this way. But in most cases it is better to download the image first by yourself and handle the displaying then (this is more or less what the OS is doing in the background). Also this method is more fail proof and allows you to respond to errors.
extension FileManager {
open func secureCopyItem(at srcURL: URL, to dstURL: URL) -> Bool {
do {
if FileManager.default.fileExists(atPath: dstURL.path) {
try FileManager.default.removeItem(at: dstURL)
}
try FileManager.default.copyItem(at: srcURL, to: dstURL)
} catch (let error) {
print("Cannot copy item at \(srcURL) to \(dstURL): \(error)")
return false
}
return true
}
}
func download() {
let storagePathUrl = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString).appendingPathComponent("image.jpg")
let imageUrl = "https://www.server.com/image.jpg"
let urlRequest = URLRequest(url: URL(string: imageUrl)!)
let task = URLSession.shared.downloadTask(with: urlRequest) { tempLocalUrl, response, error in
guard error == nil, let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
print("error")
return
}
guard FileManager.default.secureCopyItem(at: tempLocalUrl!, to: storagePathUrl) else {
print("error")
return
}
}
task.resume()
}

How would I download images from URL's with SDWebImage then append those images to a UIImage Array?

I'm using UIImageView.image in order to change the visible image on my screen.
iv.image = images[index]
The array 'images' is currently filled with local image files. However, I wish to download images from my server and then append them to the array.
private var images = [img1, img2, img3]
I have been recommended using SDWebImage (particularly SDWebImageManager or SDWebImageDownloader) to do this, however, when exploring the download and caching tutorials, all of them downloaded to a UIImageView. I cannot pass in a UIImageView into the .image extension. I couldn't find any tutorials or examples to help me achieve this. I am fairly new to swift so I do not have a vast amount of experience or understanding.
You can donwload image Async without any library as well.
Following will create a extension for imageView and it will async down image from the server just pass the respective url as parameter it will return an image.
extension UIImageView {
public func imageFromServerURL(urlString: String) {
URLSession.shared.dataTask(with: NSURL(string: urlString)! as URL, completionHandler: { (data, response, error) -> Void in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async(execute: { () -> Void in
if let imageData = data {
let image = UIImage(data: imageData)
self.image = image
}
})
}).resume()
}
}
// And you can call like this where ever required
cell.cellImageView.imageFromServerURL(urlString: banner.imageURL)

Resources