I have a UITableViewController which displays a list of NSMetaDataItems which are connected to a StoryDocument which subclasses UIDocument. Each StoryDocument has a UIImage property called image. Each row shows it's thumbnail.
I've overridden fileAttributesToWrite(to: URL, for: UIDocumentSaveOperation) to save the thumbnail as follows:
override func fileAttributesToWrite(to url: URL, for saveOperation: UIDocumentSaveOperation) throws -> [AnyHashable : Any] {
let thumbnailSize = CGSize(width: 1024, height: 1024)
if let thumbnail = image?.at(targetSize: thumbnailSize) {
return [
URLResourceKey.hasHiddenExtensionKey : true,
URLResourceKey.thumbnailDictionaryKey : [
URLThumbnailDictionaryItem.NSThumbnail1024x1024SizeKey : thumbnail
]
]
} else {
return [:]
}
}
I set the thumbnail in func tableView(_:, cellForRowAt:) where url is the url metadataItems.value(forAttribute: NSMetadataItemURLKey)
do {
var thumbnailDictionary: AnyObject?
let nsurl = url as NSURL
try nsurl.getPromisedItemResourceValue(&thumbnailDictionary, forKey: URLResourceKey.thumbnailDictionaryKey)
cell.thumbnailImageView.image = thumbnailDictionary?[URLThumbnailDictionaryItem.NSThumbnail1024x1024SizeKey] as? UIImage
} catch {
cell.thumbnailImageView.image = nil
}
If I change the StoryDocument's image in a detail view and then return to the UITableViewController, the original thumbnail (unchanged) is still shown, even though I'm telling the UITableView to reloadData(). The image only updates about 10 seconds later, when the next .NSMetadataQueryDidFinishGathering or .NSMetadataQueryDidUpdate notification comes in. Is there a good way to get the recently changed thumbnail for the cell rather than the old one?
I'm using Swift 4, although I expect I could adapt suggestions in Swift 3 too.
Many thanks in advance.
Related
I'm struggling to download pictures from firebase storage to an UIImage in swift 5.
I can well upload them. When I tried to retrieve picture, the UIImage display a black screen.
here my function which return the UIImage
import UIKit
import Firebase
func getImageEvent (imagePath : String) -> UIImage? {
var myImage : UIImageView?
//Access to the storage
let storageRef = Storage.storage().reference(withPath: imagePath)
storageRef.getData(maxSize: 1 * 1024 * 1024) {(data, error) in
if let error = error {
print(error.localizedDescription)
return
}
if let data = data {
print(data.description)
myImage?.image = UIImage(data: data)
}
}
return myImage?.image
}
//Call the function
getImageEvent (imagePath :"9U4BoXgBgTTgbbJCz0zy/eventMainImage.jpg")
In the console, I can well see a value for print(data.description).
By default, there is an image in the UIImageView. When call the function, the default image is replaced by a black screen.
Could you please help me to understand the mistake ?
many thanks
There's a number of ways to go about this but a brief description of the issue first:
The return statement within the closure will execute way before that image is downloaded - Firebase functions are asynchronous and code has to be crafted in a way that allows for time to download and get data from the internet. So - don't try to return data from asynchronous functions.
Here's the code re-written with a completion handler. That handler will be called only after the image is fully downloaded.
func getImageEvent (imagePath: String, completion: #escaping(UIImage) -> Void) {
var myImage : UIImageView?
let storageRef = Storage.storage().reference(withPath: imagePath)
storageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print(error.localizedDescription)
return
}
if let data = data {
if let myImage = UIImage(data: data) {
completion(myImage)
}
}
}
}
and the key is how to call that function. Note this code awaits the data (UIImage) to be passed back to it within it's closure and lets you know that getting the image was complete.
self.getImageEvent(imagePath: "9U4BoXgBgTTgbbJCz0zy/eventMainImage.jpg", completion: { theImage in
print("got the image!")
})
You should add additional error checking in case the image was not downloaded or myImage was nil. Passing back an error message along with the nil myImage would be one option, or making the object passed back as an optional and then checking for nil within self.downloadImageAtPath would be another.
To complete the solution, below the code used to in tableView to get picture in a particular cell
getImageEvent(imagePath: myArray[indexPath.row].partyImagePath) { (image) in
cell.partyImage.image = image
}
Im trying to get tableview cells with auto resizing images to work. Basically I want the image width in the cell to always be the same, and the height to change in accordance with the aspect ratio of the image.
I have created a cell class, which only has outlets for a label, imageView and a NSLayoutConstraint for the height of the image. I have some async methods to download an image and set it as the image for the cell imageView. Then the completion handle gets called and I run the following code to adjust the height constraint to the correct height:
cell.cellPhoto.loadImageFromURL(url: photos[indexPath.row].thumbnailURL, completion: {
// Set imageView height to the width
let imageSize = cell.cellPhoto.image?.size
let maxHeight = ((self.tableView.frame.width-30.0)*imageSize!.height) / imageSize!.width
cell.cellPhotoHeight.constant = maxHeight
cell.layoutIfNeeded()
})
return cell
And here is the UIImageView extension I wrote which loads images:
func loadImageFromURL(url: String, completion: #escaping () -> Void) {
let url = URL(string: url)
makeDataRequest(url: url!, completion: { data in
DispatchQueue.main.async {
self.image = UIImage(data: data!)
completion()
}
})
}
And the makeDataRequest function which it calls:
func makeDataRequest(url: URL, completion: #escaping (Data?) -> Void) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { data, response, error in
if error == nil {
let response = response as? HTTPURLResponse
switch response?.statusCode {
case 200:
completion(data)
case 404:
print("Invalid URL for request")
default:
print("Something else went wrong in the data request")
}
} else {
print(error?.localizedDescription ?? "Error")
}
})
task.resume()
}
This works for all the cells out of frame, but the imageviews in the cells in the frame are small. Only when I scroll down and then back up again do they correctly size. How do I fix this? I know other people have had this issue but trying their fixes did nothing.
I had to sorta recreate the problem to understand what was going on. Basically you need to reload the tableview. I would do this when a picture finishes downloading.
In the view controller that has the table view var. Add this to the viewDidLoad() function.
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
//Create a notification so we can update the list from anywhere in the app. Good if you are calling this from an other class.
NotificationCenter.default.addObserver(self, selector: #selector(loadList), name: NSNotification.Name(rawValue: "loadList"), object: nil)
}
//This function updates the cells in the table view
#objc func loadList(){
//load data here
self.tableView.reloadData()
}
Now, when the photo is done downloading, you can notify the viewcontroller to reload the table view by using the following,
func loadImageFromURL(url: String, completion: #escaping () -> Void) {
let url = URL(string: url)
makeDataRequest(url: url!, completion: { data in
DispatchQueue.main.async {
self.image = UIImage(data: data!)
completion()
//This isn't the best way to do this as, if you have 25+ pictures,
//the list will pretty much freeze up every time the list has to be reloaded.
//What you could do is have a flag to check if the first 'n' number of cells
//have been loaded, and if so then don't reload the tableview.
//Basically what I'm saying is, if the cells are off the screen who cares.
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "loadList"), object: nil)
}
})
}
Heres something I did to have better Async, see below.
My code as follows, I didn't do the resizing ratio thing like you did but the same idea applies. It's how you go about reloading the table view. Also, I personally don't like writing my own download code, with status code and everything. It isn't fun, why reinvent the wheel when someone else has done it?
Podfile
pod 'SDWebImage', '~> 5.0'
mCell.swift
class mCell: UITableViewCell {
//This keeps track to see if the cell has been already resized. This is only needed once.
var flag = false
#IBOutlet weak var cellLabel: UILabel!
#IBOutlet weak var cell_IV: UIImageView!
override func awakeFromNib() { super.awakeFromNib() }
}
viewController.swift (Click to see full code)
I'm just going to give the highlights of the code here.
//Set the image based on a url
//Remember this is all done with Async...In the backgorund, on a custom thread.
mCell.cell_IV.sd_setImage(with: URL(string: ViewController.cell_pic_url[row])) { (image, error, cache, urls) in
// If failed to load image
if (error != nil) {
//Set to defult
mCell.cell_IV.image = UIImage(named: "redx.png")
}
//Else we got the image from the web.
else {
//Set the cell image to the one we downloaded
mCell.cell_IV.image = image
//This is a flag to reload the tableview once the image is done downloading. I set a var in the cell class, this is to make sure the this is ONLY CALLED once. Otherwise the app will get stuck in an infinite loop.
if (mCell.flag != true){
DispatchQueue.main.asyncAfter(deadline: .now() + 0.025){ //Nothing wrong with a little lag.
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "loadList"), object: nil)
mCell.flag = true
}
}
}
}
I am parsing a JSON within my viewDidLoad method. One of the keys within this JSON is the image URL, which goes into a a string array called "allCImages"
This is just a string. Therefore to populate the image into the cell, in my cellForRowAt method, I have the following:
cell.vcCellImage.downloadImage(from: allCImages[indexPath.section])
Note: vcCellImage is the IBOutlet of my cell image view.
The "downloadImage" method is part of the following extension:
extension UIImageView {
func downloadImage(from imgURL: String!) {
let theUrl = URLRequest(url: URL(string: imgURL)!)
// 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 = vc1ImageCache.object(forKey: imgURL! as NSString) {
self.image = imageToCache
print("Image is in Cache")
return
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: theUrl) { (data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
// create UIImage
let imageToCache = UIImage(data: data!)
// add image to cache
vc1ImageCache.setObject(imageToCache!, forKey: imgURL! as NSString)
self.image = imageToCache
}
}
task.resume()
}
This is working almost perfectly. For example:
1) If I scroll down my tableview slowly, all the cells contain the correct image
2) If I scroll up my tableview, slowly or quickly, all the cells contain the correct image. This is proven by the fact that my console is printing the following:
Image is in Cache
Image is in Cache
Image is in Cache
I.e, the tableview is getting my image from the cache (since to scroll up, I must have scrolled down before)
3) The issue is if I scroll down my tableview really quickly, on the first attempt. Since the image has not cached yet, the cell will display the wrong image, before changing to the correct image. Classic problem
Therefore I am missing this small piece of logic. How to resolve this?
EDIT: I tried this but the issue remains:
class VCTableViewCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
vcCellImage.image = nil
}
This occurs because of
1- cell dequeueing : cells are re-used inside the tableView
2- when you scroll before a request happens it may cause a new 1 with same url
Best option is using SDWebImage
I have faced the similar issue.
I have fixed this issue by cancelling the image request in the prepareForReuse method.
Can you try the same?
first off all if you are appending your api or any data like this just remove this
var arr = [string]()
viewDidLoad()
{
arr.append("s","sd","sd)
}
accept this
var arr = [string]()
viewWillAppear()
{
arr.removeAll()
//call api
//append Data
arr.append("s","sd","sd)
}
I have similar problem then I solve it like this, may be it helpful for you also.
I'm trying to download images from my firebase database and load them into collectionviewcells. The images download, however I am having trouble having them all download and load asynchronously.
Currently when I run my code the last image downloaded loads. However, if I update my database the collection view updates and the new last user profile image also loads in but the remainder are missing.
I'd prefer to not use a 3rd party library so any resources or suggestions would be greatly appreciated.
Here's the code that handles the downloading:
func loadImageUsingCacheWithUrlString(_ urlString: String) {
self.image = nil
// checks cache
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
//download
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
//error handling
if let error = error {
print(error)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
self.image = downloadedImage
}
})
}).resume()
}
I believe the solution lies somewhere in reloading the collectionview I just don't know where exactly to do it.
Any suggestions?
EDIT:
Here is where the function is being called; my cellForItem at indexpath
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: userResultCellId, for: indexPath) as! FriendCell
let user = users[indexPath.row]
cell.nameLabel.text = user.name
if let profileImageUrl = user.profileImageUrl {
cell.profileImage.loadImageUsingCacheWithUrlString(profileImageUrl)
}
return cell
}
The only other thing that I believe could possibly affect the images loading is this function I use to download the user data, which is called in viewDidLoad, however all the other data downloads correctly.
func fetchUser(){
Database.database().reference().child("users").observe(.childAdded, with: {(snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let user = User()
user.setValuesForKeys(dictionary)
self.users.append(user)
print(self.users.count)
DispatchQueue.main.async(execute: {
self.collectionView?.reloadData()
})
}
}, withCancel: nil)
}
Current Behavior:
As for the current behavior the last cell is the only cell that displays the downloaded profile image; if there are 5 cells, the 5th is the only one that displays a profile image. Also when I update the database, ie register a new user into it, the collectionview updates and displays the newly registered user correctly with their profile image in addition to the old last cell that downloaded it's image properly. The rest however, remain without profile images.
I know you found your problem and it was unrelated to the above code, yet I still have an observation. Specifically, your asynchronous requests will carry on, even if the cell (and therefore the image view) have been subsequently reused for another index path. This results in two problems:
If you quickly scroll to the 100th row, you are going to have to wait for the images for the first 99 rows to be retrieved before you see the images for the visible cells. This can result in really long delays before images start popping in.
If that cell for the 100th row was reused several times (e.g. for row 0, for row 9, for row 18, etc.), you may see the image appear to flicker from one image to the next until you get to the image retrieval for the 100th row.
Now, you might not immediately notice either of these are problems because they will only manifest themselves when the image retrieval has a hard time keeping up with the user's scrolling (the combination of slow network and fast scrolling). As an aside, you should always test your app using the network link conditioner, which can simulate poor connections, which makes it easier to manifest these bugs.
Anyway, the solution is to keep track of (a) the current URLSessionTask associated with the last request; and (b) the current URL being requested. You can then (a) when starting a new request, make sure to cancel any prior request; and (b) when updating the image view, make sure the URL associated with the image matches what the current URL is.
The trick, though, is when writing an extension, you cannot just add new stored properties. So you have to use the associated object API to associate these two new stored values with the UIImageView object. I personally wrap this associated value API with a computed property, so that the code for retrieving the images does not get too buried with this sort of stuff. Anyway, that yields:
extension UIImageView {
private static var taskKey = 0
private static var urlKey = 0
private var currentTask: URLSessionTask? {
get { objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
private var currentURL: URL? {
get { objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
func loadImageAsync(with urlString: String?, placeholder: UIImage? = nil) {
// cancel prior task, if any
weak var oldTask = currentTask
currentTask = nil
oldTask?.cancel()
// reset image view’s image
self.image = placeholder
// allow supplying of `nil` to remove old image and then return immediately
guard let urlString = urlString else { return }
// check cache
if let cachedImage = ImageCache.shared.image(forKey: urlString) {
self.image = cachedImage
return
}
// download
let url = URL(string: urlString)!
currentURL = url
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
self?.currentTask = nil
// error handling
if let error = error {
// don't bother reporting cancelation errors
if (error as? URLError)?.code == .cancelled {
return
}
print(error)
return
}
guard let data = data, let downloadedImage = UIImage(data: data) else {
print("unable to extract image")
return
}
ImageCache.shared.save(image: downloadedImage, forKey: urlString)
if url == self?.currentURL {
DispatchQueue.main.async {
self?.image = downloadedImage
}
}
}
// save and start new task
currentTask = task
task.resume()
}
}
Also, note that you were referencing some imageCache variable (a global?). I would suggest an image cache singleton, which, in addition to offering the basic caching mechanism, also observes memory warnings and purges itself in memory pressure situations:
class ImageCache {
private let cache = NSCache<NSString, UIImage>()
private var observer: NSObjectProtocol?
static let shared = ImageCache()
private init() {
// make sure to purge cache on memory pressure
observer = NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: nil
) { [weak self] notification in
self?.cache.removeAllObjects()
}
}
deinit {
NotificationCenter.default.removeObserver(observer!)
}
func image(forKey key: String) -> UIImage? {
return cache.object(forKey: key as NSString)
}
func save(image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key as NSString)
}
}
A bigger, more architectural, observation: One really should decouple the image retrieval from the image view. Imagine you have a table where you have a dozen cells using the same image. Do you really want to retrieve the same image a dozen times just because the second image view scrolled into view before the first one finished its retrieval? No.
Also, what if you wanted to retrieve the image outside of the context of an image view? Perhaps a button? Or perhaps for some other reason, such as to download images to store in the user’s photos library. There are tons of possible image interactions above and beyond image views.
Bottom line, fetching images is not a method of an image view, but rather a generalized mechanism of which an image view would like to avail itself. An asynchronous image retrieval/caching mechanism should generally be incorporated in a separate “image manager” object. It can then detect redundant requests and be used from contexts other than an image view.
As you can see, the asynchronous retrieval and caching is starting to get a little more complicated, and this is why we generally advise considering established asynchronous image retrieval mechanisms like AlamofireImage or Kingfisher or SDWebImage. These guys have spent a lot of time tackling the above issues, and others, and are reasonably robust. But if you are going to “roll your own,” I would suggest something like the above at a bare minimum.
I was hoping I could have some advice. I'm making a quiz app, which contains images. The quiz has 4 answer boxes with a single image view. I can download the images into the quiz via parse.com, however I now want to be able to click on the image which then goes to a separate view controller so you can zoom into the image in full screen. I have been able to call the pffile and segue it, however my app crashes when I try and getdatainbackgroundwithblock in the second view controller.
How do you think I should segue the image?
EDIT
Yes sorry. So the crash I was getting was an error at code=1 address=0x48, which occurred at line +55 of getdatainbackgroundwithblock.
I have pasted snipped of the code I was using below.
I used this to call the image in the first view and make it into an image in my PFImageView.
`for object : PFObject in objectsArray as! [PFObject] {
let image = object["Image"] as! PFFile
let transimage = image
image.getDataInBackgroundWithBlock {
(imageData:NSData?, error:NSError?) -> Void in
if error == nil {
let finalimage = UIImage(data: imageData!)
// self.imageView.append(finalimage!)
self.imageView.image = final image`
I then passed it to my second view controller like this.
`override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if(segue.identifier == "ZoomImage"){
// Get the new view controller using [segue destinationViewController].
var detailScene = segue.destinationViewController as! ZoomImageViewController
detailScene.currentObject = transimage as! PFFile
NSLog("checkone")
}
}`
In my second view controller I tried to make it back into an image like this.
` if let object = currentObject {
NSLog("(currentObject)")
var initialThumbnail = UIImage(named: "question")
NSLog("check1")
imageView.image = initialThumbnail
NSLog("check2")
NSLog("check3")
object.getDataInBackgroundWithBlock {
(imageData:NSData?, error:NSError?) -> Void in
if error == nil {
NSLog("check4")
let finalimage = UIImage(data: imageData!)
// self.imageView.append(finalimage!)
self.imageView.image = finalimage
NSLog("check5")`
I would get an NSLog of 3, and then the code would break. The NSLog for the currentObject also brought back a PFFile, so I believed it to be true. Where am I going wrong? And is there a better way to pass it?
You were right, I didn't need getdata. I got rid of that and instead of seguing my pffile I segued the pfobject carrying the image and the text. I then called the object as a file and put it in the uiimageview. I'll post the code I used in a bit incase anyone wants it.
Why you call getDataInBackgroundWithBlock in both view controllers ?
You already fetch the data in your PFFile instance so no need to call it twice. It's could be the issue.