Recently I have been having a little trouble with my ios messenger. I fetched only text messages at first and everything worked perfectly. When I tried to fetch an image from parse I succeeded; however, the feed was not in the right order.
It seems like its ignoring "query.orderByAscending" entirely...
fetchMessages()
{
currentUser = PFUser.currentUser()!
let query = PFQuery(className:"Messages")
query.whereKey("convoid", equalTo:convoid)
query.orderByAscending("createdAt")
query.cachePolicy = .NetworkElseCache
query.findObjectsInBackgroundWithBlock {
(objects: [PFObject]?, error: NSError?) -> Void in
if error == nil {
dispatch_async(dispatch_get_main_queue()) {
if let objects = objects {
for object in objects {
if(object["fileType"] as? String == "photo"){
if(object["senderId"] as? String == self.currentUser.objectId!){
let userImageFile = object["file"] as! PFFile
userImageFile.getDataInBackgroundWithBlock {
(imageData: NSData?, error: NSError?) -> Void in
if error == nil {
let imageddata = UIImage(data:imageData!)
let chatBubbleData = ChatBubbleData(text: "", image:imageddata, date: object.createdAt, type: .Mine)
self.addChatBubble(chatBubbleData)
self.chatBubbleDatas.append(chatBubbleData)
}
}
}else{
let userImagefile = object["file"] as! PFFile
userImagefile.getDataInBackgroundWithBlock {
(imageData: NSData?, error: NSError?) -> Void in
if error == nil {
let imageddata = UIImage(data:imageData!)
let chatBubbleData = ChatBubbleData(text: "", image:imageddata, date: object.createdAt, type: .Opponent)
self.addChatBubble(chatBubbleData)
self.chatBubbleDatas.append(chatBubbleData)
}
}
}
}else{
if(object["senderId"] as? String == self.currentUser.objectId!){
let chatBubbleData = ChatBubbleData(text: object["text"] as? String, image:nil, date: object.createdAt, type: .Mine)
self.addChatBubble(chatBubbleData)
self.chatBubbleDatas.append(chatBubbleData)
}else{
let chatBubbleData = ChatBubbleData(text: object["text"] as? String, image:nil, date: object.createdAt, type: .Opponent)
self.addChatBubble(chatBubbleData)
self.chatBubbleDatas.append(chatBubbleData)
}
}
}
}
}
} else {
print("Error: \(error!) \(error!.userInfo)")
}
}
self.messageCointainerScroll.contentSize = CGSizeMake(CGRectGetWidth(messageCointainerScroll.frame), lastChatBubbleY + internalPadding)
self.addKeyboardNotifications()
}
Everything works well besides the fact that the message view is not presenting all the messages in the right order. In fact, all the text messages are in the right order but an message image always comes after no matter what the case, regardless of the date createdAt. I think it has to do something with loading; however I am knew to swift and I am not completely sure. Any insights on a fix or a reference please share!
My first guess is that it has to do with the placement of your call to do operations on the main queue:
dispatch_async(dispatch_get_main_queue()) {
You probably only want to do that when you need to update the user interface at the end. The issue is that the operations that take longer may get processed after the operations that take less time since the entire block is operating in the background to begin with (on a different queue, so you don't know what the scheduling is like on the main queue...).
Upon further inspection it appears you have a bunch of other background calls inside your block e.g.:
userImagefile.getDataInBackgroundWithBlock
I have written code like this using Parse and JSQMessagesViewController which I assume is what you're doing. I strongly suggest you find ways to decouple your user interface updates from your model updates over the network. What you have right now is not only hard to debug but also probably causing your problem in some kind of hard to detect asynchronous way.
It is actually using query.orderByAscending("createdAt") to get the objects. Just add print(objects) inside query.findObjectsInBackgroundWithBlock and you should see all your objects printed in the right order.
The incorrect order is a result of loading the images. When you get a text object, you immediately append the data. But when the object is an image, you call userImageFile.getDataInBackgroundWithBlock which downloads the image before calling the block which is where you append the data.
What you would need to do is append a placeholder for the image so it is in the correct position then update it when the image has finished downloading.
As a side note, you may want to look into subclassing PFObject, it will get rid of all the as? if statements and get rid of the Pyramid of Doom.
Related
I have an app that has species and photos. I am adding cloudKit to the app. I have a working solution, but now I need to add a completion handler as if the user downloads new species that include images, this takes some time (of course depending on how many images). However, the app allows the user to work during most of this process as it runs in the background.
The issue is if an image is not yet fully downloaded and the user select that species the app crashes, naturally.
I need to input a completion handler (or if someone has a better idea) that will allow me to use an activity indicator until the full process is completed. I found a few examples, but they don't take into account multiple download processes, like my images and thumbnails.
Here is my code. Note that I have removed some of the irrelevant code to reduce the amount shown.
func moveSpeciesFromCloud() {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: RemoteRecords.speciesRecord, predicate: predicate)
CKDbase.share.privateDB.perform(query, inZoneWith: nil) {
records, error in
if error != nil {
print(error!.localizedDescription)
} else {
guard let records = records else { return }
for record in records {
DispatchQueue.main.async {
self.remoteVersion = record[RemoteSpecies.remoteSpeciesVersion] as! Int
self.remoteSpeciesID = record[RemoteSpecies.remoteSpeciesID] as! Int
self.speciesDetail = AppDelegate.getUserDatabase().getSpeciesDetails(self.remoteSpeciesID)
self.localVersion = self.speciesDetail.version
// being sure that remote version is newer than local version
if self.localVersion >= self.remoteVersion {
print("Species version not newer")
} else {
self.commonNameLabel = record[RemoteSpecies.remoteCommonName] as! String
self.speciesLabel = record[RemoteSpecies.remoteSpeciesName] as! String
self.genusLabel = record[RemoteSpecies.remoteGenusName] as! String
self.groupLabel = record[RemoteSpecies.remoteGroupName] as! String
self.subGroupLabel = record[RemoteSpecies.remoteSubGroupName] as! String
self.speciesDetailsLabel = record[RemoteSpecies.remoteSpeciesDetails] as! String
// Here I sync records to SQLite, but removed code as not relevant.
// now syncing Photos, Thumbs, Groups, SubGroups and Favorties
self.syncPhotosFromCloud(self.remoteSpeciesID)
self.syncThumbsFromCloud(self.remoteSpeciesID)
}
}
}
}
}
}
Here is the code for the Thumbnails (Images are same process)
func syncThumbsFromCloud(_ id: Int) {
let predicate = NSPredicate(format: "thumbSpeciesID = \(id)")
let query = CKQuery(recordType: RemoteRecords.thumbsRecord, predicate: predicate)
CKDbase.share.privateDB!.perform(query, inZoneWith: nil)
{
records, error in
if error != nil {
print(error!.localizedDescription)
} else {
guard let records = records else { return }
for record in records {
DispatchQueue.main.async {
self.thumbName = (record.object(forKey: RemoteThumbs.remoteThumbName) as? String)!
self.thumbID = (record.object(forKey: RemoteThumbs.remoteThumbID) as? Int)!
if let asset = record[RemoteThumbs.remoteThumbFile] as? CKAsset,
let data = try? Data(contentsOf: (asset.fileURL)),
let image = UIImage(data: data)
{
let filemgr = FileManager.default
let dirPaths = filemgr.urls(for: .documentDirectory,
in: .userDomainMask)
let fileURL = dirPaths[0].appendingPathComponent(self.thumbName)
if let renderedJPEGData = image.jpegData(compressionQuality: 1.0) {
try! renderedJPEGData.write(to: fileURL)
}
}
// syncing records to SQLite
AppDelegate.getUserDatabase().syncThumbsFromCloudToSQLite(id: self.thumbID, name: self.thumbName, speciesID: id)
}
}
}
}
}
I call it here on SyncVC:
#IBAction func syncCloudToDevice(_ sender: Any) {
let cloudKit = CloudKit()
cloudKit.moveSpeciesFromCloud()
cloudKit.moveFavoritessFromCloud()
}
If I missed a detail, please let me know.
Any assistance would be greatly appreciated.
I'm kind of concerned that both the previous answers don't help answer your question.. One is asking you to restructure your database and the other is asking you to become dependent on a third-party library.
My suggestion would be to make your perform(_:inZoneWith:) into a synchronous operation so that you can easily perform one after another. For example:
func performSynchronously(query: CKQuery) throws -> [CKRecord] {
var errorResult: Error?
var recordsResult: [CKRecord]?
let semaphore = DispatchSemaphore(value: 0)
CKDbase.share.privateDB!.perform(query, inZoneWith: nil) { records, error in
recordsResult = records
errorResult = error
semaphore.signal()
}
// Block this thread until `semaphore.signal()` occurs
semaphore.wait()
if let error = errorResult {
throw error
} else {
return recordsResult ?? []
}
}
Ensure that you call this from a background thread so as to not block your UI thread! For example:
// ... start your activity indicator
DispatchQueue(label: "background").async {
do {
let records1 = try performSynchronously(query: CKQuery...)
// parse records1
let records2 = try performSynchronously(query: CKQuery...)
// parse records2
DispatchQueue.main.async {
// stop your activity indicator
}
} catch let e {
// The error e occurred, handle it and stop the activity indicator
}
}
Of course, please just use this code as inspiration on how to use a semaphore to convert your asynchronous operations into synchronous ones. Here's a good article that discusses semaphores in depth.
Well, in general that sort of things are easy to do with RxSwift. You set activity indicator to on/off in .onSubscribe() and .onTerminated(), respectively, and you get the end result in subscriber/observer when it is ready. Specifically for CloudKit, you can use RxCloudKit library.
Is there a reason why you made the pictures a separate record type? I would just add the thumbnail and the full photo to the Species record type:
thumbnail = Bytes data type (1MB max)
photo = Asset data type (virtually limitless)
That way when you do your initial Species query, you will instantly have your thumbnail available, and then you can access the CKAsset like you are currently doing and it will download in the background. No second query needed which will make your code simpler.
Title says everything. I'm just unable to download an image from Firebase Storage dir. Here is the snippet of the code which calls the function for setting data and it also calls the function which tries to download the picture:
for element in Dict {
if let itemDict = element.value as? [String:AnyObject]{
let name = itemDict["name"] as! String
let price = itemDict["price"] as! Float
let imageObject = itemDict["image"] as! NSDictionary
let hash = imageObject["hash"] as! String
let storageDir = imageObject["storageDir"] as! String
let image:UIImage = self.downloadImageProductFromFirebase(append: hash)!
let product = Product(name: name, image: image, imageName:hash, price: price, storageDir : storageDir)
self.productList.append(product)
}
}
print(Dict)
self.myTable.reloadData()
And here is the code which tries to download the image:
func downloadImageProductFromFirebase(append:String) -> UIImage?{
let gsReference = Storage.storage().reference(forURL: "gs://fridgeapp-3e2c6.appspot.com/productImages/productImages/" + append)
var image : UIImage?
gsReference.downloadURL(completion: { (url, error) in
if error != nil {
print(error.debugDescription)
return
}
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
if error != nil {
print(error.debugDescription)
return
}
guard let imageData = UIImage(data: data!) else { return }
DispatchQueue.main.async {
image = imageData
}
}).resume()
})
return image
}
But, for some reason, it crashes just when calling this last function, saying that "fatal error: unexpectedly found nil while unwrapping an Optional value". I tried to use the debugger, and I found out that Firebase reference to Storage variable says "variable not available".
Could someone of you guys help me with this? I think I read the Firebase doc about a hundred times, and still can't get the point.
Thank you!
Downloading an image from a remote server is an asynchronous task, that means that the result is not immediately available. This is the reason that gsReference.downloadURL accepts a completion callback as an argument, and has no return value.
Since your function (downloadImageProductFromFirebase) is simply a wrapper to gsReference.downloadURL, it should also accept a completion callback as an argument, and should not have a return value (i.e. remove the -> UIImage?).
When you call self.downloadImageProductFromFirebase pass in a closure that receives the image, finds the index of the corresponding product in productList, and sets itself as the cell's image (assuming you're showing the image in the cell).
See this answer for how to asynchronously set cell images.
I have a tableView which I want to fill with a list of items provided by a web service. The service returns a JSON object with status (success or failure) and shows (an array of strings).
In viewDidLoad I call the custom method getShowsFromService()
func getShowsFromService() {
// Send user data to server side
let myURL = NSURL(string: "https://myurl.com/srvc/shows.php")
// Create session instance
let session = NSURLSession.sharedSession()
var json:NSDictionary = [:]
// Create the task
let task = session.dataTaskWithURL(myURL!) { //.dataTaskWithRequest(request) {
(data, response, error) in
guard let data = data else {
print("Error: \(error!.code)")
print("\(error!.localizedDescription)")
return
}
do {
json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as! NSDictionary
} catch {
print (error)
}
let sts = json["status"] as! NSString
print("\(sts)")
}
// Resume the task so it starts
task.resume()
let shows = json["shows"] as! NSArray
for show in shows {
let thisshow = show as! String
showsArray.append(thisshow)
}
// Here I get "fatal error: unexpectedly found nil while unwrapping an Optional value"
}
The method receives the JSON object and puts it into a dictionary. Then I want to use that dictionary to call json['shows'] in order to get to the array of shows which I want to store in an instance variable called showsArray. The idea is to use showsArray in tableView(cellForRowAtIndexPath) in order to fill in the data.
The problem is that I can't get the Dictionary into the variable. If I try to do it inside the task, I get an error that says I need to call self.showsArray and if I do, the data doesn't go inside the array. If I do it outside the task I get an error because it says I'm trying to force unwrap a nil value.
How can I get the Dictionary created within the task out into the showsArray var?
The dataTaskWithURL method makes an async call, so as soon as you do task.resume() it will jump to the next line, and json["shows"] will return nil as the dictionary is empty at this point.
I would recommend moving that logic to a completion handler somewhere in your class. Something along the lines of:
func getShowsFromService() {
let myURL = NSURL(string: "https://myurl.com/srvc/shows.php")
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(myURL!, completionHandler: handleResult)
task.resume()
}
//-handle your result
func handleResult(data: NSData?, response: NSURLResponse?, error: NSError?) {
guard let data = data else {
print("Error: \(error!.code)")
print("\(error!.localizedDescription)")
return
}
do {
if let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as! NSDictionary {
if let shows = json["shows"] as! NSArray {
//- this is still in a separate thread
//- lets go back to the main thread!
dispatch_async(dispatch_get_main_queue(), {
//- this happens in the main thread
for show in shows {
showsArray.append(show as! String)
}
//- When we've got our data ready, reload the table
self.MyTableView.reloadData()
self.refreshControl?.endRefreshing()
});
}
}
} catch {
print (error)
}
}
The snippet above should serve as a guide (I dont have access to a playground atm).
Note the following:
as soon as the task completes (asynchronously -> different thread) it will call the new function handleResult which will check for errors and if not, it will use the dispatcher to perform your task on the main thread. I'm assuming showsArrays is a class property.
I hope this helps
EDIT:
As soon as you fetch your data you need to reload the table (updated code above). You can use a refresh control (declare it as a class property).
var refreshControl: UIRefreshControl!
Then when you finish getting your data you can refresh:
self.MyTableView.reloadData()
self.refreshControl?.endRefreshing()
This will call your delegate methods to populate the rows and sections.
func loadImages() {
var query = PFQuery(className: "CollegeCover")
query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if (error == nil) {
let imageObjects = objects! as [PFObject]
for object in objects! {
let thumbNail = object["image"] as! PFFile
thumbNail.getDataInBackgroundWithBlock{ (objects, error) -> Void in
if (error == nil) {
let imageObjects = objects as? [NSData?]
let image = UIImage(data:objects!)
self.userImageView.image = image
print(image)
}}}
}
else{
print("Error in retrieving \(error)")
}
}//findObjectsInBackgroundWithblock - end
}
am trying to retrieve an image which is stored in parse.com's server but i don't know why am getting an error , and am not sure that my approach for getting the image is right or not so please help me if any body knows how to do it rightly or what am missing or doing wrong the error am getting is
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
I might be wrong, but I think that the only reason you get a nil result is because you are using an incorrect column name, i.e. "image". Are you sure this is the exact naming of your column in Parse?
Once you're past that error, you should be able to get the image to load fine since your code seems OK. You could however also consider PFImageView and save yourself the trouble of the extra coding since you can specify a PFFile object to the its file property. See here for an example.
First, to be simple, how do I change a blank UIImage view to an image I have stored in Parse? This is my code so far
var query = PFQuery(className:"Content")
query.getObjectInBackgroundWithId("mlwVJLH7pa") {
(post: PFObject?, error: NSError?) -> Void in
if error == nil && post != nil {
//content.image = UIImage
} else {
println(error)
}
}
On top of just replacing the blank UIImageView, how may I make the image that it is replaced with random? I assume I can't use an objectId anymore, because that is specific to the row that it represents.
I would first retreive the objectIds from parse with getObjectsInBackgroundWithBlock, and then select a random objectId from that array in a variable called objectId. That way you save the user from querying every object from parse and using a lot of data doing it.
Second I would
getObjectInBackgroundWithId(objectId)
if error == nil {
if let image: PFFile = objectRow["image"] as? PFFile{
image.getDataInBackgroundWithBlock {
(imageData: NSObject?, error: NSError?) Void in ->
if let imageData = imageData {
let imageView = UIImageView(image: UIImage(data: imageData))
}
}
}
At least this works for me.
First off, you'll want to use query.findObjectsInBackgroundWithBlock instead of using query.getObjectInBackgroundWithId.
This will get you all of your PFObjects. Grab a random PFObject from the array it returns and now you have a single random PFObject that you can set the content.image to.
Let's say your PFObject has an 'image' attribute as a PFFile (what you should be using to store images with Parse.)
Simply convert the PFFile into a UIImage by doing something similar to the following:
if let anImage = yourRandomPFObject["image"] as? PFFile {
anImage.getDataInBackgroundWithBlock { (imageData: NSData?, error: NSError?) -> Void in
let image = UIImage(data:imageData)
content.image = image
}
}