Swift - Using Async Method to Load PHPickerResult - ios

I am trying to load a user selected image from photos using PHPickerViewController's delegate method. I know that I can do that with result.itemProvider.loadObject; however, I want to use an async version of that method that does not require a completion handler. This is what I tried:
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
Task {
do {
for result in results {
let x = try await result.itemProvider.loadItem(forTypeIdentifier: String(describing: UIImage.self)) as? UIImage
}
} catch {
print("parsing_error")
}
}
}
I am getting a parsing error. To be honest, I'm not sure how itemProvider.loadItem works exactly, and I've had trouble finding much info on it. Any recommendations?

I don't know why your code doesn't work. (It doesn't work for me either, for what it's worth.) However, I did determine code that does work, although you might not consider it to be an improvement over just using loadObject with a callback:
Task {
do {
let url = try await item.loadItem(forTypeIdentifier: "public.image") as! URL
let data = try Data(contentsOf: url)
if let image = UIImage(data: data) {
print("Image loaded: \(image)")
} else {
print("Error loading image!");
}
} catch let error {
print("async loadItem failed: \(error)")
}
}
Note: I tried to use UIImage(contentsOfFile: url.absoluteString) instead of loading the URL into a Data object and then loading the UIImage from that. That didn't work, and I don't know why. All I know is that UIImage(contentsOfFile:) returned nil.

Related

How to download images async for WidgetKit

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().

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

Completion Handler to trigger collectionView Reload once Images are downloaded - Xcode [SWIFT]

I'm using a code I got off here to download some images and present them in a collection view.
Heres the code:
func downloadImage (url: URL, completion: () -> Void){
let session = URLSession(configuration: .default)
let downloadPicTask = session.dataTask(with: url) { (data, response, error) in
if let e = error {
print("Error downloading image \(e)")
} else {
if let res = response as? HTTPURLResponse {
print("Downloaded image with response code \(res.statusCode)")
if let imageData = data {
let image = UIImage(data: imageData)
self.globalImages.append(image!)
print("what does this say?-->", self.globalImages.count)
} else {
print("Couldn't get image: Image is nil")
}
} else {
print("Couldn't get response code for some reason")
}
}
}
completion()
downloadPicTask.resume()
}
And I'm calling the download image in view did load where URL is the URL. (this URL works and I can download image).
downloadImage(url: url) { () -> () in
collectionView.ReloadData()
}
The completion handler I've tried calls reloadData() way too soon. I'm wanting it to be called when the image is finished downloading? so then the image can be displayed as soon as it's downloaded.
What is wrong with this code?
Please help!
You would call the completion handler as soon as you have the image. So, in your code:
if let imageData = data {
let image = UIImage(data: imageData)
self.globalImages.append(image!)
print("what does this say?-->", self.globalImages.count)
// call the completion handler here
Be aware, though, that you are likely to have other issues caused by data sharing across multiple threads, and also that your idea of storing the downloaded images successively in an array (globalImages) is not likely to work correctly.

Why won't my image parse into my imageView?

I am trying to parse a image from a url the url is
https://a.ppy.sh/9795284
it doesn't have a specific image extension as far as i can tell, it's just a link my current code (which works for getting the username and when I print the user_id i do get 9795284 so I know the code works (I also already get other information I wanted to get since as the username however I can not for the life of me get the users image to show here is my code for dealing with parsing
fetchCoursesJSON { (res) in
switch res {
case .success(let playerinfo):
playerinfo.forEach({ (player) in
print(player.username)
DispatchQueue.main.async {
self.myLabel.text = player.username
self.avatar.image = UIImage(named: "https://a.ppy.sh/\(player.user_id)")
}
})
case .failure(let err):
print("Failed to fetch courses:", err)
}
}
I expected the output to show the users profile pick in the avatar image but it does not it's just blank.
You need to download the image first, you can do this by using a lib like SDWebImage, AlamofireImage, Kingfisher or using the native URLSession
The UIImage(named:) is used when the resource is in your assets.
The UIImage(named:) will attempt to retrieve the image from your app.. not from the internet. Here is some code that will help you retrieve the image from your url:
guard let url = URL(string: yourUrlString ?? "") else { return }
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) -> Void in
guard let data = data, error == nil else { return }
let image = UIImage(data: data)
DispatchQueue.main.async {
self.avatar.image = image
}
}).resume()

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()
}

Resources