I am using Rob's implementation of AsynchronousOperation
Following is my AsynchronousOperation subclass
class AsynOperation : Operation {
// Same implementation as mentioned in Rob's answer
}
I'm trying to make a thumbnail for images/videos. I've wrapped the thumbnail creation functions inside an NSOperation
class AssetDocFetchOperation : AsynOperation, AssetDocGrabberProtocol {
var cacheKey: URL?
var url : String
var image : UIImage?
init(url : String, cacheKey : URL?) {
self.url = url
self.cacheKey = cacheKey
}
}
Image Generator
class AssetImageGrabber : AssetDocFetchOperation{
let client = NetworkClient()
override init(url : String,cacheKey : URL?) {
super.init(url: url,cacheKey : cacheKey)
// name = cacheKey?.absoluteString
}
override func main() {
guard let docURL = URL(string: self.url) else {
self.finish()
return
}
if let cKey = cacheKey ,let imageData = MemoryCache.shareInstance.object(forKey: cKey.absoluteString) {
self.image = UIImage(data: imageData)
self.finish()
return
}
client.shouldCache = false
let service = AssetDocFetchService(url:docURL )
client.request(customHostService: service) {[weak self] (result) in
guard let this = self else {return}
switch result {
case .success(let data) :
if let imageData = data, let i = UIImage(data: imageData){
if let cKey = this.cacheKey {
MemoryCache.shareInstance.set(object: imageData , forKey: cKey.absoluteString, cost: 1)
}
this.image = i
this.finish()
}
case .failure(let e):
print(e)
this.image = nil
this.finish()
}
}
}
override func cancel() {
super.cancel()
client.cancel()
}
}
Video thumbnail creator
class AssetVideoThumbnailMaker : AssetDocFetchOperation {
private var imageGenerator : AVAssetImageGenerator?
override init(url : String,cacheKey : URL?) {
super.init(url: url,cacheKey : cacheKey)
//name = cacheKey?.absoluteString
}
override func main() {
guard let docURL = URL(string: self.url) else {
self.finish()
return
}
if let cKey = cacheKey ,let imageData = MemoryCache.shareInstance.object(forKey: cKey.absoluteString) {
self.image = UIImage(data: imageData)
self.finish()
return
}
generateThumbnail(url: docURL) {[weak self] (image) in
self?.image = image
self?.finish()
}
}
override func cancel() {
super.cancel()
imageGenerator?.cancelAllCGImageGeneration()
}
private func generateThumbnail(url : URL ,completion: #escaping (UIImage?) -> Void) {
let asset = AVAsset(url: url)
imageGenerator = AVAssetImageGenerator(asset: asset)
let time = CMTime(seconds: 1, preferredTimescale: 60)
let times = [NSValue(time: time)]
imageGenerator?.generateCGImagesAsynchronously(forTimes: times, completionHandler: { [weak self] _, image, _, _, _ in
if let image = image {
let uiImage = UIImage(cgImage: image)
if let cKey = self?.cacheKey , let data = uiImage.pngData() {
MemoryCache.shareInstance.set(object: data , forKey: cKey.absoluteString, cost: 1)
}
completion(uiImage)
} else {
completion(nil)
}
})
}
}
I'll create NSOperation from cellForItemAt method
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let file : AssetFile = // set the file here
....
let grabOperation = taskMaker(fromDocFile : file)
cell.assetFile = grabOperation
if grabOperation.state.value == .initial {
start(operation: grabOperation)
}
return cell
}
func taskMaker(fromDocFile file : AssetFile)->AssetDocFetchOperation{
switch file.fileType {
case .image:
return AssetImageGrabber(url : file.path ?? "",cacheKey: file.cacheKeyURL)
case .video :
return AssetVideoThumbnailMaker(url : file.path ?? "",cacheKey: file.cacheKeyURL)
}
}
I'll add them to the operation queue as follows
lazy var docsOperations : OperationQueue = {
let queue = OperationQueue()
queue.name = "assetVideoOperations"
queue.maxConcurrentOperationCount = 3
return queue
}()
lazy var docsOperationInProgress : [AssetGrabOperation] = []
func start(operation : AssetGrabOperation){
if !docsOperationInProgress.contains(operation) && !operation.task.isFinished && !operation.task.isExecuting {
docsOperationInProgress.append(operation)
docsOperations.addOperation(operation.task)
}
}
For failed/timed out request, I've retry method
func reloadDocument(atIndex: IndexPath?) {
if let index = atIndex {
let tile : AssetFile = // get file here
let grabOperation = // get current opration
remove(operation: grabOperation)
reloadFile(atIndexPath: index) // i'll recreate the operation with taskMaker(fromDocFile : file)
documentList.reloadItems(at: [index])
}
}
The remove operation method
func remove(operation :AssetGrabOperation ) {
operation.task.cancel()
docsOperationInProgress = docsOperationInProgress.filter({ return $0 == operation ? false : true })
}
The problem is, if I scroll back and forth in my UICollectionView the app crashes throwing the following error. (Sometimes when I call the reloadDocument as well)
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSOperationQueue addOperation:]: operation is already enqueued on a queue'
I've also found a solutions/workaround by checking the names of the operation
let alreadyOn = docsOperations.operations.filter({
return $0.name == operation.task.name
})
// i'll assign the cacheURL to name while the init of opration
if alreadyOn.count == 0 {
docsOperationInProgress.append(operation)
docsOperations.addOperation(operation.task)
}
I'm not sure whether this approch is good or not. What am i doing wrong?
After a long time to fix this issue, I did make an extension for Operation to remove dependencies before starting the new Operation, This could be a workaround solution 😅
extension Operation {
public func removeAllDependencies() {
dependencies.forEach {
$0.removeAllDependencies()
removeDependency($0) }
}
}
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 trying to load image for the particular cell for whose indexpath the URL is present. Have function to download the image and send it through call back method, but after callback the other cells are also getting loaded by the downloaded image. Thanks in Adv.
Here is the code sample. In method cellForItemAtIndexPath
let stationCollectionViewcell : StationCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "stationCell", for: indexPath) as! StationCollectionViewCell
if imageURL.contains("http") {
self.loadImageWithURL(url: URL(string: station.imageURL)!) { (image) in
stationCollectionViewcell.radioImgView.image = image
}
} else if imageURL != "" {
stationCollectionViewcell.radioImgView.image = UIImage(named: "station-therockfm")
} else {
stationCollectionViewcell.radioImgView.image = UIImage(named: "stationImage")
}
And the function that will download the image
func loadImageWithURL(url: URL, callback: #escaping (UIImage) -> ()) {
print("This is getting excuted loadImageWithURL")
let session = URLSession.shared
let downloadTask = session.downloadTask(with: url, completionHandler: {
url, response, error in
if error == nil && url != nil {
if let data = NSData(contentsOf: url!) {
if let image = UIImage(data: data as Data) {
DispatchQueue.main.async(execute: {
callback(image)
})
}
}
}
})
downloadTask.resume()
}
You should reset image before reusing cell, because it is reused with previous image before new is downloaded. Also, you should compare saved url with url in callback, because callback may return when cell is reused.
// reset image
override func prepareForReuse() {
super.prepareForReuse()
radioImgView.image = nil // set nil or default image
}
Add url to callback
func loadImageWithURL(url: URL, callback: #escaping (UIImage, URL) -> ()) {
print("This is getting excuted loadImageWithURL")
let session = URLSession.shared
let downloadTask = session.downloadTask(with: url, completionHandler: {
url, response, error in
if error == nil && url != nil {
if let data = NSData(contentsOf: url!) {
if let image = UIImage(data: data as Data) {
DispatchQueue.main.async(execute: {
callback(image, url!)
})
}
}
}
})
downloadTask.resume()
}
Compare url
let stationCollectionViewcell : StationCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "stationCell", for: indexPath) as! StationCollectionViewCell
if imageURL.contains("http") {
let url = URL(string: station.imageURL)!
self.loadImageWithURL(url: url) { (image, callbackUrl) in
guard url == callbackUrl else { return }
stationCollectionViewcell.radioImgView.image = image
}
} else if imageURL != "" {
stationCollectionViewcell.radioImgView.image = UIImage(named: "station-therockfm")
} else {
stationCollectionViewcell.radioImgView.image = UIImage(named: "stationImage")
}
I hope, it will run Fine.
import UIKit
let imageCache = NSCache<AnyObject, AnyObject>()
class CustomImageView : UIImageView {
var imgUrlString : String?
func loadImageWithURL(urlString : String) {
imgUrlString = urlString
guard let url = URL(string: urlString) else { return }
image = nil
if let imgFromCache = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = imgFromCache
return
}
URLSession.shared.dataTask(with: url) { (data, resposne, error) in
if error != nil {
print(error)
return
}
DispatchQueue.main.sync {
let imageToCache = UIImage(data: data!)
imageCache.setObject(imageToCache!, forKey: urlString as AnyObject)
if self.imgUrlString == urlString {
self.image = imageToCache
}
}
}.resume()
}
}
Intialize your instance with this Class.
let thumbnailImageView : CustomImageView = {
let imgView = CustomImageView()
imgView.translatesAutoresizingMaskIntoConstraints = false
imgView.image = #imageLiteral(resourceName: "taylor_swift_blank_space")
imgView.contentMode = .scaleAspectFill
imgView.clipsToBounds = true
return imgView
}()
Cells are reusing each time, also you have async call in the controller. In the downloading completion, you need to reload cell by IndexPath.
Also, reset image in prepareForReuse
override func prepareForReuse() {
super.prepareForReuse()
stationCollectionViewcell.radioImgView.image = nil
}
The best solution with async images it's to have cache
let imageCache: NSCache<NSString, UIImage> = NSCache<NSString, UIImage>()
after loading has been completed save the image:
imageCache.setObject(image, forKey: url as NSString)
and next time, when cell appears, check is image already exists in imageCache
if yes, use image from the cache, if not, download an image
Code should looks like this:
if imageURL.contains("http") {
if let image = imageCache.object(forKey: station.imageURL as NSString) {
stationCollectionViewcell.radioImgView.image = image
} else {
self.loadImageWithURL(url: URL(string: station.imageURL)!) { [weak self] (image) in
self?.imageCache.setObject(image, forKey: url as NSString)
self?.collectionView.collectionView.reloadItems(at: [indexPath])
}
}
} else if imageURL != "" {
stationCollectionViewcell.radioImgView.image = UIImage(named: "station-therockfm")
} else {
stationCollectionViewcell.radioImgView.image = UIImage(named: "stationImage")
}
I am trying to cancel my NSOperationsQueue and remove all the operations from the queue. I called the method "cancelAllOperations()" but this doesnt remove the operation from queue, I am aware that this method only adds a flag to the operation.
How do I go about removing all operations from my queue? Or prevent the operation queue from executing operations that have a flag?
private let mediaDownloadOperationQueue: NSOperationQueue = {
let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 1
queue.qualityOfService = .Background
return queue
}()
func startDownloadProcess() {
guard downloadSwitchState == true else { return }
let context = DataBaseManager.sharedInstance.mainManagedObjectContext
let mediasToDownload = self.listOfMediaToDownload(context)
for media in mediasToDownload {
downloadMedia(media)
}
}
private func downloadMedia(media: Media) {
//check if operation already exist
for operation in mediaDownloadOperationQueue.operations {
let operation = operation as! MediaDownloadOperation
if operation.media.objectID == media.objectID { return }
}
//HERE I am adding the operation to queue
mediaDownloadOperationQueue.addOperation(MediaDownloadOperation(media: media))
}
//EDIT: Here is my MediasDownloadOperation
import Foundation
import Alamofire
class MediaDownloadOperation: BaseAsyncOperation {
let media: Media
init(media: Media) {
self.media = media
super.init()
}
override func main() {
guard let mediaSourceURI = media.sourceURI
else {
self.completeOperation()
return
}
var filePath: NSURL?
let destination: (NSURL, NSHTTPURLResponse) -> (NSURL) = {
(temporaryURL, response) in
if let directoryURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first, let suggestedFilename = response.suggestedFilename {
filePath = directoryURL.URLByAppendingPathComponent("\(suggestedFilename)")
return filePath!
}
return temporaryURL
}
RequestManager.sharedAlamofireManager.download(.GET, mediaSourceURI, destination: destination).response {
(request, response, data, error) -> Void in
if let filePath = filePath where error == nil {
let saveToCameraRoll = self.media.saveToCameraRoll?.boolValue == true
self.media.fileLocation = filePath.lastPathComponent
self.media.saveToCameraRoll = false
DataBaseManager.sharedInstance.save()
if saveToCameraRoll {
self.media.saveMediaToCameraRoll(nil)
}
}
self.completeOperation()
NSNotificationCenter.defaultCenter().postNotificationName(MediaDownloadManager.MediaIsDownloadedNote, object: nil)
}
}
}
Adding a conditional to test to see if operation has cancelled seemed to have solved the problem:
RequestManager.sharedAlamofireManager.download(.GET, mediaSourceURI, destination: destination).response {
(request, response, data, error) -> Void in
//Only download if logged in
if (self.cancelled == false){
print("RAN operation!")
if let filePath = filePath where error == nil {
let saveToCameraRoll = self.media.saveToCameraRoll?.boolValue == true
self.media.fileLocation = filePath.lastPathComponent
self.media.saveToCameraRoll = false
DataBaseManager.sharedInstance.save()
if saveToCameraRoll {
self.media.saveMediaToCameraRoll(nil)
}
}
self.completeOperation()
NSNotificationCenter.defaultCenter().postNotificationName(MediaDownloadManager.MediaIsDownloadedNote, object: nil)
}else{
print("did not run operation! was cancelled!")
self.completeOperation()
}
}
I'm developing a chat app, I'm having problem showing the Avatar to my JSQMessagesViewController
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
var avatar = UIImage()
let people = FIRDatabase.database().reference().child("people").child(senderId)
people.observeEventType(.Value, withBlock: {
snapshot -> Void in
let dict = snapshot.value as! Dictionary<String, AnyObject>
let imageUrl = dict["profileImage"] as! String
if imageUrl.hasPrefix("gs://") {
FIRStorage.storage().referenceForURL(imageUrl).dataWithMaxSize(INT64_MAX, completion: { (data, error) in
if let error = error {
print("Error downloading: \(error)")
return
}
avatar = UIImage.init(data: data!)!
})
}
})
let AvatarJobs = JSQMessagesAvatarImageFactory.avatarImageWithPlaceholder(avatar, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
return AvatarJobs
}
The problem here is, when I'm trying to pull the image of the sender from firebase, I'm getting a blank image, but when i try to use this let AvatarJobs = JSQMessagesAvatarImageFactory.avatarImageWithPlaceholder(UIImage(named: "icon.png"), diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault)) it's working fine, What do you think is the problem here? Thanks!
If I may suggest an alternative? Why don't you have a dictionary:
var avatars = [String: JSQMessagesAvatarImage]()
let storage = FIRStorage.storage()
And use the following function:
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource!
{
let message = messages[indexPath.row]
return avatars[message.senderId]
}
And create the avatars in viewDidLoad (or where ever )
createAvatar(senderId, senderDisplayName: senderDisplayName, user: currentUser, color: UIColor.lightGrayColor())
with a function
func createAvatar(senderId: String, senderDisplayName: String, user: FBUser, color: UIColor)
{
if self.avatars[senderId] == nil
{
//as you can see, I use cache
let img = MyImageCache.sharedCache.objectForKey(senderId) as? UIImage
if img != nil
{
self.avatars[senderId] = JSQMessagesAvatarImageFactory.avatarImageWithImage(img, diameter: 30)
// print("from cache")
}
else if let photoUrl = user.pictureURL where user.pictureURL != ""
{
// the images are very small, so the following methods work just fine, no need for Alamofire here
if photoUrl.containsString("https://firebasestorage.googleapis.com")
{
self.storage.referenceForURL(photoUrl).dataWithMaxSize(1 * 1024 * 1024) { (data, error) -> Void in
if (error != nil)
{
//deal with error
}
else
{
let newImage = UIImage(data: data!)
self.avatars[senderId] = JSQMessagesAvatarImageFactory.avatarImageWithImage(newImage, diameter: 30)
MyImageCache.sharedCache.setObject(newImage!, forKey: senderId, cost: data!.length)
}
}
}
else if let data = NSData(contentsOfURL: NSURL(string:photoUrl)!)
{
let newImage = UIImage(data: data)!
self.avatars[senderId] = JSQMessagesAvatarImageFactory.avatarImageWithImage(newImage, diameter: 30)
MyImageCache.sharedCache.setObject(newImage, forKey: senderId, cost: data.length)
}
else
{
//etc. blank image or image with initials
}
}
}
else
{
//etc. blank image or image with initials
}
}
for Cache I have a custom class
import Foundation
class MyImageCache
{
static let sharedCache: NSCache =
{
let cache = NSCache()
cache.name = "MyImageCache"
cache.countLimit = 200 // Max 200 images in memory.
cache.totalCostLimit = 20*1024*1024 // Max 20MB used.
return cache
}()
}
Let me know if that helps
I would suggest trying to isolate your problems. I don't know if the issue is with JSQMessagesAvatarImageFactory I think the issue may be that you do not have the image downloaded by the time the cell needs to be displayed. I would make sure that you are getting something back from fireBase before you try and set it to your avatar. A closure is normally how I do this something like
func getImageForUser(id: String, completiion() -> Void) {
//Add your logic for retrieving from firebase
let imageFromFirebase = firebaserReference.chiledWithID(id)
completion(image)
}
Then in your
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
var avatarImage = JSQAavatarImage()
getImageForUser {
self.avatarImage = JSQMessagesAvatarImageFactory.avatarImageWithPlaceholder(imageFromFirebase, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
self.collectionView.reloadItemAtIndexPath(indexPath)
}
That way it waits till the response is back and then reloads the cell once it is there.
Let me know if you have any other questions.
So I am making a network request. I parse the JSON to custom Objects. In these objects there are urls which are suppose to bring back images. One of the URL returns an error message (404) so there ins't anything there! How can I set a default image in its place and stop my app from crashing? Here is my code! Thanks
import UIKit
class HomepageCollectionViewController: UICollectionViewController {
var imageCache = NSCache()
var hingeImagesArray = [HingeImage]()
var arrayToHoldConvertedUrlToUIImages = [UIImage]()
var task: NSURLSessionDataTask?
override func viewDidLoad() {
super.viewDidLoad()
// Makes the network call for HingeImages
refreshItems()
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return hingeImagesArray.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("imageReuseCell", forIndexPath: indexPath) as! ImageCollectionViewCell
let image = hingeImagesArray[indexPath.row]
if let imageURL = image.imageUrl {
if let url = NSURL(string: imageURL) {
//settingImageTpChache
if let myImage = imageCache.objectForKey(image.imageUrl!) as? UIImage {
cell.collectionViewImage.image = myImage
}else {
// Request images asynchronously so the collection view does not slow down/lag
self.task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
// Check if there is data returned
guard let data = data else {
return
}
// Create an image object from our data and assign it to cell
if let hingeImage = UIImage(data: data){
//cachedImage
self.imageCache.setObject(hingeImage, forKey: image.imageUrl!)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.collectionViewImage.image = hingeImage
//append converted Images to array so we can send them over to next view - only proble in that the only images converted at the ones you scrool to which is retarted
self.arrayToHoldConvertedUrlToUIImages.append(hingeImage)
print(self.arrayToHoldConvertedUrlToUIImages)
})
}
})
task?.resume()
}
}
}
return cell
}
you can check if error is not nil then set deafult image .
self.task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
if error != nil {
cell.collectionViewImage.image = UIImage(named:"default_image")
return
}
...
Try this:
let imageCache = NSCache<AnyObject, AnyObject>()
extension UIImageView {
func loadImageUsingCacheWithUrl(urlString: String) {
self.image = nil
// check for cache
if let cachedImage = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = cachedImage
return
}
// download image from url
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) -> Void in
var image:UIImage
if error == nil {
if(UIImage(data: data!) != nil){
image = UIImage(data: data!)!
} else {
image = UIImage(named: "DefaultImage")!
}
} else {
print(error ?? "load image error")
return
}
DispatchQueue.main.async(execute: { () -> Void in
imageCache.setObject(image, forKey: urlString as AnyObject)
self.image = image
})
}).resume()
}
}
The key point is with 404 return message, data task error is still = nil and this time you must check UIImage(data: data!) != nil to prevent a “fatal error: unexpectedly found nil while unwrapping an Optional value”