I have 3 entities ( User, Album and Photo) with the following attributes.
I cant read data from Photo ( Album 1:<->>many Photo).
This is how i save my data
var managedContext: NSManagedObjectContext!
var currentUser: User!
#IBAction func saveButtonTapped(_ sender: Any) {
let album = Album(context: managedContext)
album.albumMainPhoto = photoArray[0].jpegData(compressionQuality: 1.0) as! NSData
album.albumDescription = photoDescriptionArray[0]
album.numberOfPhotos = Int16(photoArray.count)
let photos = Photo(context: managedContext)
photos.colorAsHex = "hexTest"
photos.photo = photoArray[0].jpegData(compressionQuality: 1.0) as! NSData
photos.photoDescription = "descTest"
photos.photoStyle = "styleTest"
album.addToAlbumToPhotos(photos)
currentUser?.addToUserToAlbum(album)
do {
try managedContext.save()
} catch let error as NSError {
print("save error: \(error), description: \(error.userInfo)")
}
dismiss(animated: true, completion: nil)
}
and i retrieve data by this
guard let album = currentUser?.userToAlbum?[indexPath.row] as? Album,
let albumTitle = album.albumDescription as String?,
let albumPhotosCount = album.numberOfPhotos as Int16?,
let albumMainPhoto = album.albumMainPhoto as NSData? else {
return
}
guard let photos = Albums?.albumToPhotos?[indexPath.row] as? Photo,
let photodesc = photos.photoDescription as String?,
let photoImage = photos.photo as NSData?
else {
return
}
The first option ( guard let album) works perfectly , i cant read albumDescription, numberOfPhotos and see image albumMainPhoto. When i try to use this on photos it can't read data. I am still new to coreData, so i might do something wrong.
I was able to get data with this :
guard let album = currentUser?.userToAlbum?[indexPath.row] as? Album,
let albumTitle = album.albumDescription as String?,
let albumPhotosCount = album.numberOfPhotos as Int16?,
let albumMainPhoto = album.albumMainPhoto as NSData?,
let photos = album.albumToPhotos?[indexPath.row] as? Photo,
let photosDes = photos.photoDescription as String?
else {
return
}
Related
I have tableview with label, imageView (for image, gif & video thumbnail). I am sure that doing something wrong and I can't handle its completion handler due to which the app is hanged and gets stuck for a long time.
My model is like,
struct PostiisCollection {
var id :String?
var userID: String?
var leadDetails : NSDictionary?
var company: NSDictionary?
var content: String?
init(Doc: DocumentSnapshot) {
self.id = Doc.documentID
self.userID = Doc.get("userID") as? String ?? ""
self.leadDetails = Doc.get("postiiDetails") as? NSDictionary
self.company = Doc.get("company") as? NSDictionary
self.content = Doc.get("content") as? String ?? ""
}
}
I wrote in my view controller for fetch this,
var postiisCollectionDetails = [PostiisCollection]()
override func viewDidLoad() {
super.viewDidLoad()
let docRef = Firestore.firestore().collection("PostiisCollection").whereField("accessType", isEqualTo: "all_access")
docRef.getDocuments { (querysnapshot, error) in
if let doc = querysnapshot?.documents, !doc.isEmpty {
print("Document is present.")
for document in querysnapshot!.documents {
_ = document.documentID
if let compCode = document.get("company") as? NSDictionary {
do {
let jsonData = try JSONSerialization.data(withJSONObject: compCode)
let companyPost: Company = try! JSONDecoder().decode(Company.self, from: jsonData)
if companyPost.companyCode == AuthService.instance.companyId ?? ""{
print(AuthService.instance.companyId ?? "")
if (document.get("postiiDetails") as? NSDictionary) != nil {
let commentItem = PostiisCollection(Doc: document)
self.postiisCollectionDetails.append(commentItem)
}
}
} catch {
print(error.localizedDescription)
}
DispatchQueue.main.async {
self.tableView.isHidden = false
self.tableView.reloadData()
}
}
}
}
}
}
I need to check for the index path with image view is either image or gif or video with different parameters, I tried with tableview delegate and datasource method by,
extension AllAccessPostiiVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postiisCollectionDetails.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AllAccessCell", for: indexPath)
let label1 = cell.viewWithTag(1) as? UILabel
let imagePointer = cell.viewWithTag(3) as? UIImageView
let getGif = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "gif") as? NSArray
let getPhoto = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "photo") as? NSArray
let getVideo = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "video") as? NSArray
label1?.text = "\(arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "title"))"
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
print(arrGif[0])
let gifURL : String = "\(arrGif[0])"
let imageURL = UIImage.gifImageWithURL(gifURL)
imagePointer?.image = imageURL
playButton?.isHidden = true
}
if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
print(arrPhoto[0])
let storageRef = Storage.storage().reference(forURL: arrPhoto[0])
storageRef.downloadURL(completion: { (url, error) in
do {
let data = try Data(contentsOf: url!)
let image = UIImage(data: data as Data)
DispatchQueue.main.async {
imagePointer?.image = image
playButton?.isHidden = true
}
} catch {
print(error)
}
})
}
if getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let videoURL = URL(string: arrVideo[0])
let asset = AVAsset(url:videoURL!)
if let videoThumbnail = asset.videoThumbnail{
SVProgressHUD.dismiss()
imagePointer?.image = videoThumbnail
playButton?.isHidden = false
}
}
}
}
If I run, the app hangs in this page and data load time is getting more, some cases the preview image is wrongly displayed and not able to handle its completion
As others have mentioned in the comments, you are better of not performing the background loading in cellFroRowAtIndexPath.
Instead, it's better practice to add a new method fetchData(), where you perform all the server interaction.
So for example:
// Add instance variables for fast access to data
private var images = [UIImage]()
private var thumbnails = [UIImage]()
private func fetchData(completion: ()->()) {
// Load storage URLs
var storageURLs = ...
// Load data from firebase
let storageRef = Storage.storage().reference(forURL: arrPhoto[0])
storageRef.downloadURL(completion: { (url, error) in
// Parse data and store resulting image in image array
...
// Call completion handler to indicate that loading has finished
completion()
})
}
Now you can call fetchData() whenever you would like to refresh data and call tableview.reloadData() within the completion handler. That of course must be done on the main thread.
This approach simplifies your cellForRowAtIndexPath method.
There you can just say:
imagePointer?.image = ...Correct image from image array...
Without any background loading.
I suggest using below lightweight extension for image downloading from URL
using NSCache
extension UIImageView {
func downloadImage(urlString: String, success: ((_ image: UIImage?) -> Void)? = nil, failure: ((String) -> Void)? = nil) {
let imageCache = NSCache<NSString, UIImage>()
DispatchQueue.main.async {[weak self] in
self?.image = nil
}
if let image = imageCache.object(forKey: urlString as NSString) {
DispatchQueue.main.async {[weak self] in
self?.image = image
}
success?(image)
} else {
guard let url = URL(string: urlString) else {
print("failed to create url")
return
}
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
// response received, now switch back to main queue
DispatchQueue.main.async {[weak self] in
if let error = error {
failure?(error.localizedDescription)
}
else if let data = data, let image = UIImage(data: data) {
imageCache.setObject(image, forKey: url.absoluteString as NSString)
self?.image = image
success?(image)
} else {
failure?("Image not available")
}
}
}
task.resume()
}
}
}
Usage:
let path = "https://i.stack.imgur.com/o5YNI.jpg"
let imageView = UIImageView() // your imageView, which will download image
imageView.downloadImage(urlString: path)
No need to put imageView.downloadImage(urlString: path) in mainQueue, its handled in extension
In your case:
You can implement following logic in cellForRowAt method
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
let urlString : String = "\(arrGif[0])"
let image = UIImage.gifImageWithURL(urlString)
imagePointer?.image = image
playButton?.isHidden = true
}
else if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
let urlString = Storage.storage().reference(forURL: arrPhoto[0])
imagePointer?.downloadImage(urlString: urlString)
playButton?.isHidden = true
}
elseif getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let urlString = arrVideo[0]
imagePointer?.downloadImage(urlString: urlString)
playButton?.isHidden = false
}
If you have one imageView to reload in tableView for photo, video and gif. then use one image array to store it prior before reloading. So that your main issue of hang or stuck will be resolved. Here the main issue is each time in table view cell collection data is being called and checked while scrolling.
Now I suggest to get all photo, gifs and video (thumbnail) as one single array prior to table view reload try this,
var cacheImages = [UIImage]()
private func fetchData(completionBlock: () -> ()) {
for (index, _) in postiisCollectionDetails.enumerated() {
let getGif = postiisCollectionDetails[index].leadDetails?.value(forKey: "gif") as? NSArray
let getPhoto = postiisCollectionDetails[index].leadDetails?.value(forKey: "photo") as? NSArray
let getVideo = postiisCollectionDetails[index].leadDetails?.value(forKey: "video") as? NSArray
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
let gifURL : String = "\(arrGif[0])"
self.randomList.append(gifURL)
/////---------------------------
let imageURL = UIImage.gifImageWithURL(gifURL)
self.cacheImages.append(imageURL!)
//////=================
}
else if getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let videoURL: String = "\(arrVideo[0])"
let videoUrl = URL(string: arrVideo[0])
let asset = AVAsset(url:videoUrl!)
if let videoThumbnail = asset.videoThumbnail{
////--------------
self.cacheImages.append(videoThumbnail)
//-----------
}
self.randomList.append(videoURL)
}else if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
let photoURL : String = "\(arrPhoto[0])"
/////---------------------------
let url = URL(string: photoURL)
let data = try? Data(contentsOf: url!)
if let imageData = data {
let image = UIImage(data: imageData)
if image != nil {
self.cacheImages.append(image!)
}
else {
let defaultImage: UIImage = UIImage(named:"edit-user-80")!
self.cacheImages.append(defaultImage)
}
}
//////=================
}
else {
//-----------------
let defaultImage: UIImage = UIImage(named:"edit-user-80")!
self.cacheImages.append(defaultImage)
//--------------------
}
}
completionBlock()
}
After getting all UIImage as array where loop is being called. Now you call this function inside your viewDidLoad. So after all values in images fetched then try to call tableView like this,
override func viewDidLoad() {
self.fetchData {
DispatchQueue.main.async
self.tableView.reloadData()
}
}
}
Now atlast, you may use SDWebImage or any other background image class or download method to call those in tableView cellforRow method,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// your cell idetifier & other stuffs
if getVideo != nil {
imagePointer?.image = cacheImages[indexPath.row]
playButton?.isHidden = false
}else {
imagePointer?.image = cacheImages[indexPath.row]
// or get photo with string via SdWebImage
// imagePointer?.sd_setImage(with: URL(string: photoURL), placeholderImage: UIImage(named: "edit-user-80"))
playButton?.isHidden = true
}
return cell
}
You're handling data in a totally wrong manner. Data(contentsOf: url!) - This is wrong. You should chache the images and should download it to directory. When you convert something into data it takes place into the memory(ram) and it is not good idea when playing with large files. You should use SDWebImage kind of library to set images to imageview.
Second thing if let videoThumbnail = asset.videoThumbnail - This is also wrong. Why you're creating assets and then getting thumbnail from it? You should have separate URL for the thumbnail image for your all videos in the response of the API and then again you can use SDWebImage to load that thumbnail.
You can use SDWebImage for gif as well.
Alternative of SDWebImage is Kingfisher. Just go through both libraries and use whatever suitable for you.
I am using the Flickr API to get all albums within a collection and then using the album IDs from to get the primary photo for each of the albums. It works but the photos are not returned to me in order so the primary photo for each album does not match up to the album titles in my collection view.
func getPhotoCollection() {
let collectionURLString = "https://api.flickr.com/services/rest/?method=flickr.collections.getTree&api_key={API_KEY}&collection_id=72157676119666248&user_id={USER_ID}&format=json&nojsoncallback=1"
self.session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let task = self.session?.dataTask(with: URL(string: collectionURLString)!, completionHandler: { (data, response, error) in
let json = self.getJSONFrom(urlString: collectionURLString)
let collections = json["collections"]
let collection = collections["collection"].arrayObject as! [[String:AnyObject]]
for collectionObject in collection {
let sets = collectionObject["set"] as! [[String: AnyObject]]
for set in sets {
let albumId = set["id"] as! String
let albumTitle = set["title"] as! String
self.albumIds.append(albumId)
self.albumTitles.append(albumTitle)
}
}
self.getAlbumPrimary()
})
task?.resume()
}
func getAlbumPrimary() {
for albumId in self.albumIds {
let apiURLString = "https://api.flickr.com/services/rest/?method=flickr.photosets.getPhotos&api_key={API_KEY}&photoset_id=\(albumId)&per_page=1&user_id={USER_ID}&format=json&nojsoncallback=1"
self.session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let task = self.session?.dataTask(with: URL(string: apiURLString)!, completionHandler: { (data, response, error) in
let json = self.getJSONFrom(urlString: apiURLString)
let photos = json["photoset"]
let photo = photos["photo"].arrayObject as! [[String:AnyObject]]
let primaryPic = photo[0]
let farm = primaryPic["farm"] as! Int
let server = primaryPic["server"] as! String
let picId = primaryPic["id"] as! String
let secret = primaryPic["secret"] as! String
let urlString = String(format: "https://farm%d.static.flickr.com/%#/%#_%#_b.jpg", farm, server, picId, secret)
self.albumPrimaryURLs.append(urlString)
DispatchQueue.main.async {
self.collectionView.reloadData()
self.loaded = true
}
})
task?.resume()
}
}
This is the way multiple asynchronous tasks work ( out of order ) ,you need to create a model instead of seperate arrays then load from the data to fill the last property
class Item {
let id,title:String
var url:String?
init(id:String,title:String){
self.id = id
self.title = title
}
func loadUrl(completion:#escaping () -> () ) {
// here load and set the url
}
}
var items = [Item]() // declare main array
let albumId = set["id"] as! String
let albumTitle = set["title"] as! String
let item = Item(id:albumId,title:albumTitle)
self.items.append(item)
Then to load the collection finally
let g = DispatchGroup()
items.forEach {
g.enter()
$0.loadUrl {
g.leave()
}
}
g.notify(queue:.main) {
self.collectionView.reloadData()
self.loaded = true
}
I try to save two different images at the same time at one storage location
This is my function to save the information
var text: String = ""
var addedByUser: String?
var userImage: UIImage?
var jobImage: UIImage!
var downloadURL: String?
var userDownloadURL: String?
let ref: DatabaseReference!
init(text: String? = nil, jobImage: UIImage? = nil, addedByUser: String? = nil, userImage: UIImage? = nil) {
self.text = text!
self.jobImage = jobImage
self.addedByUser = addedByUser
self.userImage = userImage
ref = Database.database().reference().child("jobs").childByAutoId()
}
init(snapshot: DataSnapshot){
ref = snapshot.ref
if let value = snapshot.value as? [String : Any] {
text = value["text"] as! String
addedByUser = value["addedByUser"] as? String
downloadURL = value["imageDownloadURL"] as? String
userDownloadURL = value["imageUserDownloadURL"] as? String
}
}
func save() {
let newPostKey = ref.key
// save jobImage
if let imageData = jobImage.jpegData(compressionQuality: 0.5) {
let storage = Storage.storage().reference().child("jobImages/\(newPostKey)")
storage.putData(imageData).observe(.success, handler: { (snapshot) in
self.downloadURL = snapshot.metadata?.downloadURL()?.absoluteString
let postDictionary = [
"imageDownloadURL" : self.downloadURL!,
"imageUserDownloadURL" : self.userDownloadURL!,
"text" : self.text,
"addedByUser" : self.addedByUser!
] as [String : Any]
self.ref.setValue(postDictionary)
})
}
}
I tried following code
if let imageData = jobImage.jpegData(compressionQuality: 0.5), ((userImage?.jpegData(compressionQuality: 0.5)) != nil) {
But it's not working as then nothing get's saved in the database...
Do you have any ideas how I can solve it?
I believe the question is how do I upload an image to two different locations. It's unclear why there's an observe function so this answer ignores that as it may not be needed.
Starting with your code, your save function will look like this
func save() {
self.uploadImageTask(imageName: "my_image.png", toLocation: "jobImage")
self.uploadImageTask(imageName: "my_image.png", toLocation: "anotherLocation")
}
and then the upload function
func uploadImageTask(imageName: String, toLocation: String) {
let theImage = UIImage(named: imageName) //set up your image here
let data = UIImagePNGRepresentation(theImage)! //we're doing a PNG
let storage = Storage.storage()
let storageRef = storage.reference()
let locationRef = storageRef.child("images").child(toLocation)
let imageLocationRef = locationRef.child(imageName)
// Upload the file to the path "images/location/imageName"
let uploadTask = locationRef.putData(data, metadata: nil) { (metadata, error) in
guard let metadata = metadata else {
print("error while uploading")
return
}
let size = metadata.size // Metadata contains file metadata such as size, content-type.
print(size)
locationRef.downloadURL { (url, error) in
guard let downloadURL = url else {
print("an error occured after uploading and then downloading")
return
}
let x = downloadURL.absoluteString
print(x) //or build a dict and save to Firebase
}
}
}
the result is an image stored at
/images/jobImage/my_image.png
/images/anotherLocation/my_image.png
and it will also print the path to each image, which could be stored in Firebase.
I have looked at a lot of questions and I do not believe this is a result of reusing a cell as the new cells image is correct, but and existing cells image is incorrect and used to be correct. I'll post the images first so the issue is easier to understand.
I have a collectionView of image cells (similar to Instagrams user page). I'm fetching all of the data from Firebase. I get the first 12 posts on the initial loading of the screen. However, if you scroll down quickly an EXISTING cells image changes to a newly fetched image. I'm not sure why this is happening... Maybe it's a caching issue? The issue only occurs the first time you load the screen. I've tried setting the images to nil like this:
override func prepareForReuse() {
super.prepareForReuse()
self.imageView.image = UIImage()
}
This didn't help the issue though.
Here is my cellForItemAt:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "imageCell", for: indexPath) as! ImageCell
cell.indexPath = indexPath
cell.imageView.downloadImage(from: currentTablePosts[indexPath.row].pathToImage)
cell.layer.borderWidth = 1
cell.layer.borderColor = UIColor.black.cgColor
return cell
}
Image downloading and caching:
let imageCache = NSCache<NSString, UIImage>()
extension UIImageView {
func downloadImage(from imgURL: String!) {
let url = 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 = imageCache.object(forKey: imgURL! as NSString) {
self.image = imageToCache
return
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
// user an alert to display the error
if let topController = UIApplication.topViewController() {
Helper.showAlertMessage(vc: topController, title: "Error Downloading Image", message: error as! String)
}
return
}
DispatchQueue.main.async {
// create UIImage
let imageToCache = UIImage(data: data!)
// add image to cache
imageCache.setObject(imageToCache!, forKey: imgURL! as NSString)
self.image = imageToCache
}
}
task.resume()
}
}
Firebase Queries:
static func getInitialTablesPosts(tableNumber: String) {
tableReference.child(tableNumber).queryLimited(toLast: 12).observeSingleEvent(of: .value, with: { snap in
for child in snap.children {
let child = child as? DataSnapshot
if let post = child?.value as? [String: AnyObject] {
let posst = Post()
if let author = post["author"] as? String, let likes = post["likes"] as? Int, let pathToImage = post["pathToImage"] as? String, let postID = post["postID"] as? String, let postDescription = post["postDescription"] as? String, let timestamp = post["timestamp"] as? Double, let category = post["category"] as? String, let table = post["group"] as? String, let userID = post["userID"] as? String, let numberOfComments = post["numberOfComments"] as? Int, let region = post["region"] as? String {
posst.author = author
posst.likes = likes
posst.pathToImage = pathToImage
posst.postID = postID
posst.userID = userID
posst.fancyPostDescription = Helper.createAttributedString(author: author, postText: postDescription)
posst.postDescription = author + ": " + postDescription
posst.timestamp = timestamp
posst.table = table
posst.region = region
posst.category = category
posst.numberOfComments = numberOfComments
posst.userWhoPostedLabel = Helper.createAttributedPostLabel(username: author, table: table, region: region, category: category)
if let people = post["peopleWhoLike"] as? [String: AnyObject] {
for(_, person) in people {
posst.peopleWhoLike.append(person as! String)
}
}
currentTablePosts.insert(posst, at: 0)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTableCollectionView"), object: nil)
} // end of if let
}
}
})
tableReference.removeAllObservers()
}
static func getMoreTablePosts(tableNumber: String, lastVisibleKey: String) {
print("FIRED...")
let currentNumberOfPosts = currentTablePosts.count
print("Number of posts before fetiching ", currentNumberOfPosts)
print("Oldest post key ", oldestTableKeys[tableNumber] ?? "not set yet", "***********")
tableReference.child(tableNumber).queryOrderedByKey().queryEnding(atValue: lastVisibleKey).queryLimited(toLast: 12).observeSingleEvent(of: .value, with: { snap in
for child in snap.children {
let child = child as? DataSnapshot
if let post = child?.value as? [String: AnyObject] {
if let id = post["postID"] as? String {
if id == lastVisibleKey {
return
}
}
let posst = Post()
if let author = post["author"] as? String, let likes = post["likes"] as? Int, let pathToImage = post["pathToImage"] as? String, let postID = post["postID"] as? String, let postDescription = post["postDescription"] as? String, let timestamp = post["timestamp"] as? Double, let category = post["category"] as? String, let table = post["group"] as? String, let userID = post["userID"] as? String, let numberOfComments = post["numberOfComments"] as? Int, let region = post["region"] as? String {
posst.author = author
posst.likes = likes
posst.pathToImage = pathToImage
posst.postID = postID
posst.userID = userID
posst.fancyPostDescription = Helper.createAttributedString(author: author, postText: postDescription)
posst.postDescription = author + ": " + postDescription
posst.timestamp = timestamp
posst.table = table
posst.region = region
posst.category = category
posst.numberOfComments = numberOfComments
posst.userWhoPostedLabel = Helper.createAttributedPostLabel(username: author, table: table, region: region, category: category)
if let people = post["peopleWhoLike"] as? [String: AnyObject] {
for(_, person) in people {
posst.peopleWhoLike.append(person as! String)
}
}
currentTablePosts.insert(posst, at: currentNumberOfPosts)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTableCollectionView"), object: nil)
if let oldestTableKey = oldestTableKeys[tableNumber] {
if postID == oldestTableKey {
print("returning")
print("number of posts on return \(currentTablePosts.count)")
return
}
}
} // end if let
}
}
})
tableReference.removeAllObservers()
}
* UPDATE *
The image caching used here still had some issues in my experience. I have since moved on to using Kingfisher which is extremely easy to setup and use.
* OLD SOLUTION *
Found a solution based on the answer suggested in the comments.
I modified the extension I am using to cache my images. Although in the future I think I will subclass UIImageView. Here is the modified version of my code.
import UIKit
let userImageCache = NSCache<NSString, UIImage>()
let imageCache = NSCache<AnyObject, AnyObject>()
var imageURLString: String?
extension UIImageView {
public func imageFromServerURL(urlString: String, collectionView: UICollectionView, indexpath : IndexPath) {
imageURLString = urlString
if let url = URL(string: urlString) {
image = nil
if let imageFromCache = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = imageFromCache
return
}
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
if error != nil{
if let topController = UIApplication.topViewController() {
Helper.showAlertMessage(vc: topController, title: "Error Downloading Image", message: error as! String)
}
return
}
DispatchQueue.main.async(execute: {
if let imgaeToCache = UIImage(data: data!){
if imageURLString == urlString {
self.image = imgaeToCache
}
imageCache.setObject(imgaeToCache, forKey: urlString as AnyObject)// calls when scrolling
collectionView.reloadItems(at: [indexpath])
}
})
}) .resume()
}
}
func downloadImage(from imgURL: String!) {
let url = 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 = imageCache.object(forKey: imgURL! as AnyObject) as? UIImage {
self.image = imageToCache
return
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
// user an alert to display the error
if let topController = UIApplication.topViewController() {
Helper.showAlertMessage(vc: topController, title: "Error Downloading Image", message: error as! String)
}
return
}
DispatchQueue.main.async {
let imageToCache = UIImage(data: data!)
imageCache.setObject(imageToCache!, forKey: imgURL! as AnyObject)
self.image = imageToCache
}
}
task.resume()
}
func downloadUserImage(from imgURL: String!) {
let url = 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 = userImageCache.object(forKey: imgURL! as NSString) {
self.image = imageToCache
return
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
// user an alert to display the error
if let topController = UIApplication.topViewController() {
Helper.showAlertMessage(vc: topController, title: "Error Downloading Image", message: error as! String)
}
return
}
DispatchQueue.main.async {
// create UIImage
let imageToCache = UIImage(data: data!)
// add image to cache
userImageCache.setObject(imageToCache!, forKey: imgURL! as NSString)
self.image = imageToCache
}
}
task.resume()
}
}
I created the first method to cache the images for the collectionView and the other methods are still used for tableViews. The cache was also changed from let imageCache = NSCache<NSString, UIImage>() to let imageCache = NSCache<AnyObject, AnyObject>()
I am using this code to store an image in icloud, but what code do i use to retrieave it and place it in a UIImageView? I've tried everything, but it wont work?
func SaveImageInCloud(ImageToSave: UIImage) {
let newRecord:CKRecord = CKRecord(recordType: "ImageRecord")
let nsDocumentDirectory = NSSearchPathDirectory.DocumentDirectory
let nsUserDomainMask = NSSearchPathDomainMask.UserDomainMask
if let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true) {
if paths.count > 0 {
if let dirPath = paths[0] as? String {
let writePath = dirPath.stringByAppendingPathComponent("Image2.png")
UIImagePNGRepresentation(ImageToSave).writeToFile(writePath, atomically: true)
var File : CKAsset? = CKAsset(fileURL: NSURL(fileURLWithPath: writePath))
newRecord.setValue(File, forKey: "Image")
}
}
}
if let database = self.privateDatabase {
database.saveRecord(newRecord, completionHandler: { (record:CKRecord!, error:NSError! ) in
if error != nil {
NSLog(error.localizedDescription)
} else {
dispatch_async(dispatch_get_main_queue()) {
println("finished")
}
}
})
}
Just read the CKRecord that you wrote and you can get the CKAsset by reading the key Image. You can get a UIImage using the code below.
var file : CKAsset? = record.objectForKey("Image")
func image() -> UIImage? {
if let file = file {
if let data = NSData(contentsOfURL: file.fileURL) {
return UIImage(data: data)
}
}
return nil
}
After downloading the CKAsset, we need to convert the CKAsset to a UIImage. We can use the following extension (Swift 4 code):
extension CKAsset {
func toUIImage() -> UIImage? {
if let data = NSData(contentsOf: self.fileURL) {
return UIImage(data: data as Data)
}
return nil
}
}
You have to first have a way of finding the specific ImageRecord that you want to retrieve. Assuming that you have the RecordID for the ImageRecord you saved (you can get this from the record in the saveRecord completion block) you can do:
if let database = privateDatabase {
database.fetchRecordWithID(recordID, completionHandler: { (record, error) -> Void in
guard let record = record else {
print("Error retrieving record", error)
return
}
guard let asset = record["Image"] as? CKAsset else {
print("Image missing from record")
return
}
guard let imageData = NSData(contentsOfURL: asset.fileURL) else {
print("Invalid Image")
return
}
let image = UIImage(data: imageData)
imageView.image = image
})
}
(Although you would definitely want to be doing some error handling where those print()s are)
If you don't save the recordID (or probably better: the recordID.recordName so you can make another CKRecordID later), you would need some other way of finding which record you are looking for. If that's the case you'd want to look into using CKDatabase's performQuery(_:inZoneWithID:completionHandler:) method.