I have a collection view. I want to fill each cell with the images that are stored in the apps core data. However the cells only fill with the last image that was taken.
not each individual cell benign filled with each individual image.
my code is as follows:
when the user takes a photo it is compressed and stored in core data:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
picker.dismiss(animated: true)
guard let image = info[.editedImage] as? UIImage else {
print("No image found")
return
}
// print out the image size as a test
print(image.size)
//convert image to savable type for coredata
var jpegImageData = image.jpegData(compressionQuality: 1.0)
jegImage = jpegImageData!
//calling function in coredataManger to save image in data format
CoreDataManager.sharedInstance.addImage()
}
The adding to core data function that was activated above:
func addImage() {
let entityName = NSEntityDescription.entity(forEntityName: "ImageData", in: context)
let image2 = NSManagedObject(entity: entityName!, insertInto: context)
image2.setValue(jegImage, forKeyPath: "imageName")
do {
try context.save()
print("image is saved")
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
ive global variables to pass images from one VC to another
//global variables for images saved to core data
var jegImage = Data()
var theImagePulled = UIImage()
Trying to populate the collection view cell with the images stored in core data:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HomeRoomCell", for: indexPath) as! HomeRoomCell
let name = cartoonArr[indexPath.item]
//setting image shown in the colleciton view equal to the gloabl images stored in core data
let image = theImagePulled
cell.bindData(name: name, image: image)
return cell
}
So the problem is that the cell gets populated but they all populate with the same image. this image is the last image that was added to core data model, even though there are several images in the core data model. I have been playing around with trying to set up an array that stores all these core data images but i can't figure it out. any ideas?
Get rid of the global variables, which are indeed capturing the most recent image.
Use NSFetchedResultsController in your collection view.
If you’re new to Core Data, you might consider passing just a string around and displaying that, first. Then move on to images.
Related
So I have a collection view that populates three default images on each cell. The user can select images through the new PHPicker, what I want to accomplish is
to replace default images with selected photos, so the first selected image should replace the camera cell and so on ... (see images through links at the bottom)
to display the default images in case user deletes the selected images
currently when I send a new photo to the imgArray it gets displayed in a new cell before the camera cell, as I'm using insert method like so: imgArray.insert(image, at: 0).
My code:
var imgArray = [UIImage(systemName: "camera"), UIImage(systemName: "photo"), UIImage(systemName: "photo")]
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "1", for: indexPath) as! CustomCell
cell.imageView.image = imgArray[indexPath.row]
cell.imageView.tintColor = .gray
cell.imageView.layer.cornerRadius = 4
return cell
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
dismiss(animated: true, completion: nil)
for item in results {
item.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
if let image = image as? UIImage {
DispatchQueue.main.async {
self.imgArray.insert(image, at: 0)
self.collectionView.reloadData()
}
}
}
}
}
I have tried to remove the first item of the array, and then insert new photo like this:
self.imgArray.removeFirst(1)
self.imgArray.insert(image, at: 0)
self.collectionView.reloadData()
but this would work just one time as the code itself says, after that all replacement takes place just in the first cell.
So how can I get to other cells after first replacement? Any other approach that gives the same result will help me alot. Thanks in advance guys!
before selection
after selecting three images
One way to do this is to keep two image arrays, defaultImages of type [UIImage] and inputImages of type [UIImage?]. The defaultImages will hold your camera and photo images, and inputImages will hold images selected by the user. Initialize the images as:
defaultImages = [UIImage(systemName: "camera"), UIImage(systemName: "photo"), UIImage(systemName: "photo")]
inputImages: [UIImage?] = [nil, nil, nil]
To select the correct photo for index index use:
image = inputImages[index] ?? defaultImages[index]
To add a user-input image at index index use:
image: UIImage = ... // Get the image.
inputImages[index] = image
And to delete a user-input image at index index use:
inputImages[index] = nil
I have a code for getting user pic:
if let photoURL = message[Constants.MessageFields.photoURL], let URL = URL(string: photoURL),
let data = try? Data(contentsOf: URL) {
cell.userPic.image = UIImage(data: data)
}
When I'm using it, tableView lagging at scrolling.
Please help me to put this code in another thread.
Here is a good sample provided by Apple, that you can adapt for your needs:
Prefetching collection view data
Basic idea is to create AsyncFetcher for your images and put image creation code to separate operation.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else {
fatalError("Expected `\(Cell.self)` type for reuseIdentifier \(Cell.reuseIdentifier). Check the configuration in Main.storyboard.")
}
let model = models[indexPath.row]
let id = model.id
cell.representedId = id
// Check if the `asyncFetcher` has already fetched data for the specified identifier.
if let fetchedData = asyncFetcher.fetchedData(for: id) {
// The data has already been fetched and cached; use it to configure the cell.
cell.configure(with: fetchedData)
} else {
// There is no data available; clear the cell until we've fetched data.
cell.configure(with: nil)
// Ask the `asyncFetcher` to fetch data for the specified identifier.
asyncFetcher.fetchAsync(id) { fetchedData in
DispatchQueue.main.async {
/*
The `asyncFetcher` has fetched data for the identifier. Before
updating the cell, check if it has been recycled by the
collection view to represent other data.
*/
guard cell.representedId == id else { return }
// Configure the cell with the fetched image.
cell.configure(with: fetchedData)
}
}
}
return cell
}
But in your case you should use Table View prefetching
I can confirm that this approach works and (when done right) results smooth scrolling and good UX
I am trying to understand just how UICollectionView cell reuse works.
I am currently implementing a horizontally scrolling UICollectionView with large cells that take up almost the full size of the screen. There are about 100+ cells but you will only ever see ~3 at a time.
As I understand it UICollectionView cell reuse simply maintains a pool of initialized cell objects that way when one cell is out of view it can be cannibalized by a newly viewable cell. That is to say since I am using reuse the collection might only initialize ~3 actual cell objects in memory and I just will switch out their contents.
I am very worried about what this means in the case of custom cells that have image views that are based on images that need to be downloaded. Ideally I would have a scenario where every cells image is only ever downloaded once and it is only downloaded when absolutely necessary.
If there is truly a pool of my custom cell objects then this means that that is totally not happening. As each time a cell comes into view I am starting a completely new download.
How am I supposed to do this right?
The main reason I am asking this is that when scrolling (especially on the initial scroll) I do see some flickering between an image of an old cell and the image the cell is supposed to be displaying. I made a fix but I am fairly sure that it is causing the online images to be downloaded too many times. Am I doing this right?
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as? ImageCell else {
assert(false)
}
let image = data[indexPath.row]
cell.display(title: image.title, imageURL: image.imageURL)
return cell
}
And the cell
public class NewsCell: UICollectionViewCell {
private var title: UILabel = UILabel()
private var imageView: UIImageView = UIImageView()
override public init(frame: CGRect = CGRect.zero) {
super.init(frame: frame)
title.font = UIFont.systemFont(ofSize: 12, weight: .bold)
title.textColor = UIColor.white
title.textAlignment = NSTextAlignment.left
imageView.contentMode = UIViewContentMode.scaleAspectFill
imageView.clipsToBounds = true
contentView.addSubview(imageView)
imageView.addSubview(title)
// Layout constraints
}
public func display(title: String, imageURL: URL?) {
self.imageView.image = nil
self.title.text = title
if let url = imageURL {
downloadImage(from: url)
}
}
func downloadImage(from url: URL) {
getData(from: url) { data, response, error in
guard let data = data, error == nil else {
return
}
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data)
}
}
}
func getData(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
}
I am worried by the fact that I have to set the image views image to nil on display in order to prevent the flicker. Should I be doing something differently to avoid frivolous downloading of these images or does this look good?
You can use Prefetching Collection View Data to load your images earlier.
You use data prefetching when loading data is a slow or expensive process—for example when fetching data over the network. In these circumstances, perform data loading asynchronously.
It will require some changes of course. You will have to store your images separately and download them when DataSourcePrefetching method will be called. Also, then the image is downloaded, you'll need to check if there is any cell that waiting for that image. So your UICollectionView won't download anything anymore. It will show the image only, or waiting for it to be downloaded.
There is a problem with your current solution. If you'll scroll too fast, you make face a situation when because of reusing, the same UICollectionViewCell is loading a few images at once. And in this case, the user will see only the last one, and you never can tell which one it would be. To avoid this race condition, you can store the image identifier or its URL in the cell so after downloading is finished, it could check if the downloaded image is the right one.
It's okay to set image to nil while it's downloading, but you also can show to the user some UIActivityIndicatorView so he could see that some work is happening.
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 have a UICollectionView in a UIView container, and I use it for displaying images (as an array of PFFile) attached to a PFObject stored in Parse. I know I could fetch the the PFFile/image synchronously in my current implementation (see below working code for synchronous loading), but I would really like to make this an asynchronous process.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("attachedImageCell", forIndexPath: indexPath) as! AttachedImageCollectionViewCell
if let uploadImageFile = attachedImageFiles[indexPath.row] as? PFFile {
do {
var imageData: NSData
try imageData = uploadImageFile.getData()
cell.imageView.image = UIImage(data: imageData)
} catch let err as NSError {
print("error getting image file data: \(err)")
}
/*
//asynchronously getting image data - don't know how to return cell here
uploadImageFile.getDataInBackgroundWithBlock({ (imageData, error) -> Void in
if error != nil {
//TODO: show error in download
}
if imageData != nil {
cell.imageView.image = UIImage(data: imageData!)
}
})
*/
}
return cell
}
One way I have been contemplating is to have the AttachedImageCollectionViewCell object observe its UIImageView, once a UIImage file (a pointer) is assigned to it, it will go fetching the the PFFile from Parse and parse it to NSData, which is used for the UIImage.
As I am still on the learning curve of mastering Swift, I am not sure how feasible this approach might be. So I am all ears for any ideas here, thanks!
So in your code where you create the cell, you ensure, you set the image to cell asynchronously using the library-
https://github.com/natelyman/SwiftImageLoader/blob/master/ImageLoader.swift
//Create cell in usual way
//Code here to create the cell
//Load the image needed by the cell asynchronously
ImageLoader.sharedLoader.imageForUrl(urlString, completionHandler:{(image: UIImage?, url: String) in
cell.image = image
})
HTH