I have a small problem with some code here. I am trying to populate a collection view with Five Names, descriptions and Images.
I am able to successfully to download all of the above into their respected arrays.
The problem is that the first time I perform the segue the image array has zero values in it. Then I go back a page and re-enter the page to find that all of the arrays have been populated successfully....
This is really annoying. Here is my code:
//arrays of names, descriptions and images
var names:[String] = []
var descriptions: [String] = []
var imagesArray: [UIImage] = []
Heres where I get the images:
func downloadImages(){
for x in 1...5{
let url = URL(string: "https://www.imagesLocation.com/(x).png")
let task = URLSession.shared.dataTask(with: url!){(data, response, error) in
guard
let data = data,
let newImage = UIImage(data: data)
else{
print("Could not load image from URL: ",url!)
return
}
DispatchQueue.main.async {
self.imagesArray.append(newImage)
}
}
task.resume()
}
loadDataFromFirebase()
}
Heres where I download the Names and Descriptions from:
func loadDataFromFirebase() {
// Fetch and convert data
let db = Firestore.firestore()
db.collection(self.shopName).getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
return
} else {
for document in snapshot!.documents {
let name = document.get("Name") as! String
let description = document.get("Description") as! String
self.names.append(name)
self.descriptions.append(description)
}
self.setupImages() //safe to do this here as the firebase data is valid
}
}
}
Heres where I setup the collection view with the Names, Description and Images array contents:
func setupImages(){
do {
if imagesArray.count < 5 || names.count < 5 || descriptions.count < 5 {
throw MyError.FoundNil("Something hasnt loaded")
}
self.pages = [
Page(imageName: imagesArray[0], headerText: names[0], bodyText: descriptions[0]),
Page(imageName: imagesArray[1], headerText: names[1], bodyText: descriptions[1]),
Page(imageName: imagesArray[2], headerText: names[2], bodyText: descriptions[2]),
Page(imageName: imagesArray[3], headerText: names[3], bodyText: descriptions[3]),
Page(imageName: imagesArray[4], headerText: names[4], bodyText: descriptions[4]),
]
}
catch {
print("Unexpected error: \(error).")
}
}
As you can see from the image below, every array is populating successfully apart from the images array:
Here is the segue from the previous page's code:
DispatchQueue.main.async(){
self.performSegue(withIdentifier: "goToNext", sender: self)
}
Any help is welcome :)
Your question is just a variant of the classic, "Why is my asynchronous function returning empty data?" I've answered a couple of these questions, and I'll include an analogy that explains the issue. You might understand the issue already, but I'll include it anyway for future readers:
Your mom is cooking dinner and asks you to go buy a lemon.
She starts cooking, but she has no lemon!
Why? Because you haven't yet returned from the supermarket, and your
mom didn't wait.
Source
The main issue here is that you are calling loadDataFromFirebase way too early. You assume that it will execute only after your URL requests have completed, but that is not the case. Why? Because the URL requests are executed asynchronously. That is, they run on another thread instead of blocking the thread that calls dataTask.resume. This is why, as Shashank Mishra suggests, you should use a DispatchGroup. Additionally, there is no guarantee that your images will load in the order that you begin the data tasks. I have included a fix below.
Generally, I would recommend defining variables strictly in the scopes in which you need them. Keeping names, descriptions, and images at such a high scope makes it too easy to make mistakes. I suggest refactoring your functions and deleting those three class-level arrays. Instead:
func loadDataFromFirebase(images: [UIImage]) {
// same function as you posted, except make names and descriptions local variables and
// replace self.setupImages() with:
DispatchQueue.main.async {
self.setupImages(images: images, names: names, descriptions: descriptions)
}
}
func setupImages(images: [UIImage], names: [String], descriptions: [String]) {
guard images.count == 5, names.count == 5, descriptions.count == 5 else {
print("Missing data.")
return
}
self.pages = (0..<5).map({ Page(image: images[$0], header: names[$0], body: descriptions[$0]) })
// super important!!!
tableView.reloadData()
}
Finally, here is my suggestion for a thread-safe downloadImages function:
func downloadImages() {
var images = [UIImage?](repeating: nil, count: 5)
let dispatchGroup = DispatchGroup()
for i in 1...5 {
dispatchGroup.enter()
let url = URL(string: "https://www.imagesLocation.com/\(i).png")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data, let image = UIImage(data: data) else {
print("Could not load image from", url)
dispatchGroup.leave()
return
}
images[i] = image
dispatchGroup.leave()
}.resume()
}
dispatchGroup.notify(queue: .main) {
guard images.allSatisfy({$0 != nil}) else {
print("Failed to fetch all images.")
return
}
self.loadDataFromFirebase(images: images.compactMap({$0}))
}
}
As Fattie pointed out, you should use addSnapshotListener rather than getDocuments. Also, you should add the listener/get documents while downloading the images instead of after, which will be faster. However, I am not adding either to my answer because this is already quite long, and if you have trouble with it you can post another question.
You can use DispatchGroup to achieve asynchronous calls -
func downloadImages() {
let dispatchGroup = DispatchGroup()
for x in 1...5 {
dispatchGroup.enter()
let url = URL(string: "https://www.imagesLocation.com/(x).png")
let task = URLSession.shared.dataTask(with: url!){(data, response, error) in
guard
let data = data,
let newImage = UIImage(data: data)
else{
print("Could not load image from URL: ",url!)
dispatchGroup.leave()
return
}
self.imagesArray.append(newImage)
dispatchGroup.leave()
}
task.resume()
}
dispatchGroup.notify(queue: DispatchQueue.main) {
self.loadDataFromFirebase()
}
}
Call "loadDataFromFirebase()" method on getting all 5 responses as above. It will always have all images before loading it on view.
You're misunderstanding how Firebase works.
Essentially.
Don't use getDocuments. Use .addSnapshotListener
and
Basically each time the snapshot arrives, simply call .reloadData() on the table.
A full tutorial is beyond the scope of an answer here but there are many, many, tutorials around.
Just a typical fragment ...
let db = Firestore.firestore().db.collection("yourCollection")
.whereField("user", isEqualTo: uid)
.addSnapshotListener { [weak self] documentSnapshot, error in
guard let self = self else { return }
guard let ds = documentSnapshot else {
return print("error: \(error!)")
}
self.displayItems = .. that data
self.tableView.reloadData()
}
Note the .reloadData()
Also ..
It's true that you can store an image (binary data) right in Firestore.
But really never, ever, do that - it's completely useless.
Simply use the dead-easy Firebase/Storage system where you can host images for free. Then they have completely normal URLs and so on.
Full tutorial: https://stackoverflow.com/a/62626214/294884
Related
I am developing Widgets for iOS and I really don't know how to download images for the widgets.
The widget currently downloads an array of Objects, and every object has a URL of an image. The idea is that every object makes a SimpleEntry for the Timeline.
What's the best way of achieving this? I read that Widgets shouldn't use the ObservableObject. I fetch the set of objects in the timeline provider, which seems to be what Apple recommends. But do I also download the images there and I wait until all are done to send the timeline?
Any advice would be very helpful,
Yes, you should download the images in the timeline provider and send the timeline when they are all done. Refer to the following recommendation by an Apple frameworks engineer.
I use a dispatch group to achieve this.
Something like:
let imageRequestGroup = DispatchGroup()
var images: [UIImage] = []
for imageUrl in imageUrls {
imageRequestGroup.enter()
yourAsyncUIImageProvider.getImage(fromUrl: imageUrl) { image in
images.append(image)
imageRequestGroup.leave()
}
}
imageRequestGroup.notify(queue: .main) {
completion(images)
}
I then use SwiftUI's Image(uiImage:) initializer to display the images
I dont have a good solution, but I try to use WidgetCenter.shared.reloadAllTimelines(), and it make sence.
In the following code.
var downloadImage: UIImage?
func downloadImage(url: URL) -> UIImage {
var picImage: UIImage!
if self.downloadImage == nil {
picImage = UIImage(named: "Default Image")
DispatchQueue.global(qos: .background).async {
do {
let data = try Data(contentsOf: url)
DispatchQueue.main.async {
self.downloadImage = UIImage.init(data: data)
if self.downloadImage != nil {
DispatchQueue.main.async {
WidgetCenter.shared.reloadAllTimelines()
}
}
}
} catch { }
}
} else {
picImage = self.downloadImage
}
return picImage
}
Also you have to consider when to delete this picture.
This like tableView.reloadData().
I'm creating an application which are fetching data from an API. I've created the "API call" in a separate class so I can use the same call multiple times. But it does not return the value as I expect it to.
In ViewController A
let data = JsonData.init()
data.downloadJsonData(urlString: urlString) { (responseArray) in
dataArray.append(responseArray)
print(self.dataArray)
}
I'm getting the error at dataArray.append(responseArray):
Cannot convert value of type '[ResponseData]' to expected argument type 'ResponseData'
In JsonData class
class JsonData{
var dataArray:[ResponseData] = []
func downloadJsonData(urlString: String, completed: #escaping (Array<ResponseData>) -> ()){
guard let url = URL(string: urlString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else{
return
}
do{
self.dataArray = [try JSONDecoder().decode(ResponseData.self, from: data)]
//Complete task in background
DispatchQueue.main.async {
completed(self.dataArray)
}
}
catch let jsonErr{
print(jsonErr)
}
}.resume()
}
}
I assume the problem is at:
DispatchQueue.main.async{
completed(self.dataArray)
}
So I would like to return the array back to the correct class once it fetched the data from the API. What could I have done wrong? Any help would be much appreciated.
The error is clear: You are using the (wrong) API for appending a single element
Replace
dataArray.append(responseArray)
with
self.dataArray.append(contentsOf: responseArray)
Side note:
Setting and later appending the items again makes no sense. Use a local variable.
Replace
self.dataArray = [try JSONDecoder().decode(ResponseData.self, from: data)]
//Complete task in background
DispatchQueue.main.async {
completed(self.dataArray)
}
with (a different name makes it clearer)
let result = try JSONDecoder().decode(ResponseData.self, from: data)
//Complete task in background
DispatchQueue.main.async {
completed([result])
}
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.
I am trying to modify the global variable currentWeather (of type CurrentWeather) using this function, which is meant to update said variable with the information retrieved from the URL and return a bool signifying its success. However, the function is returning false, as currentWeather is still nil. I recognize that the dataTask is asynchronous, and that the task is running in the background parallel to the application, but I don't understand what this means for what I'm trying to accomplish. I also am unable to update currentWeather after the do block, as weather is no longer recognized after exiting the block. I did try using "self.currentWeather", but was told it was an unresolved identifier (perhaps because the function is also global, and there is no "self"?).
The URL is not currently valid because I took out my API key, but it is working as expected otherwise, and my CurrentWeather struct is Decodable. Printing currentWeatherUnwrapped is also consistently successful.
I did look around Stack Overflow and through Apple's official documentation and was unable to find something that answered my question, but perhaps I wasn't thorough enough. I'm sorry if this is a duplicate question. Direction to any further relevant reading is also appreciated! I apologize for the lack of conformity to best coding practices - I'm not very experienced at this point. Thank you all so much!
func getCurrentWeather () -> Bool {
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
print(currentWeatherUnwrapped)
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
return currentWeather != nil
}
When you do an asynchronous call like this, your function will return long before your dataTask will have any value to return. What you need to do is use a completion handler in your function. You can pass it in as a parameter like this:
func getCurrentWeather(completion: #escaping(CurrentWeather?, Error?) -> Void) {
//Data task and such here
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else {
completion(nil, err)
return
}
//You don't need a do try catch if you use try?
let weather = try? JSONDecoder().decode(CurrentWeather.self, from: data)
completion(weather, err)
}.resume()
}
Then calling that function looks like this:
getCurrentWeather(completion: { (weather, error) in
guard error == nil, let weather = weather else {
if weather == nil { print("No Weather") }
if error != nil { print(error!.localizedDescription) }
return
}
//Do something with your weather result
print(weather)
})
All you need is a closure.
You cant have synchronous return statement to return the response of web service call which in itself is asynchronous in nature. You need closures for that.
You can modify your answer as below. Because you have not answered to my question in comment I have taken liberty to return the wether object rather than returning bool which does not make much sense.
func getCurrentWeather (completion : #escaping((CurrentWeather?) -> ()) ){
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
CurrentWeather.currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
completion(CurrentWeather.currentWeather)
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
completion(nil)
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
}
Assuming currentWeather is a static variable in your CurrentWeather class you can update your global variable as well as return the actual data to caller as shown above
EDIT:
As pointed out by Duncan in comments below, the above code executes the completion block in background thread. All the UI operations must be done only on main thread. Hence its very much essential to switch the thread before updating the UI.
Two ways :
1- Make sure you execute the completion block on main thread.
DispatchQueue.main.async {
completion(CurrentWeather.currentWeather)
}
This will make sure that whoever uses your getCurrentWeather in future need not worry about switching thread because your method takes care of it. Useful if your completion block contains only the code to update UI. Lengthier logic in completion block with this approach will burden the main thread.
2 - Else In completion block that you pass as a parameter to getCurrentWeather whenever you update UI elements make sure you wrap those statements in
DispatchQueue.main.async {
//your code to update UI
}
EDIT 2:
As pointed out by Leo Dabus in comments below, I should have run completion block rather than guard let url = URL(string: jsonUrlString) else { return false } That was a copy paste error. I copied the OP's question and in a hurry din realize that there is a return statement.
Though having a error as a parameter is optional in this case and completely depends on how you designed your error handling model, I appreciate the idea suggested by Leo Dabus which is more general approach and hence updating my answer to have error as a parameter.
Now there are cases where we may need to send our custom error as well for example if guard let data = data else { return } returns false rather than simply calling return you may need to return a error of your own which says invalid input or something like that.
Hence I have taken a liberty to declare a custom errors of my own and you can as well use the model to deal with your error handling
enum CustomError : Error {
case invalidServerResponse
case invalidURL
}
func getCurrentWeather (completion : #escaping((CurrentWeather?,Error?) -> ()) ){
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/"
guard let url = URL(string: jsonUrlString) else {
DispatchQueue.main.async {
completion(nil,CustomError.invalidURL)
}
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
if err != nil {
DispatchQueue.main.async {
completion(nil,err)
}
return
}
guard let data = data else {
DispatchQueue.main.async {
completion(nil,CustomError.invalidServerResponse)
}
return
}
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
CurrentWeather.currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
DispatchQueue.main.async {
completion(CurrentWeather.currentWeather,nil)
}
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
DispatchQueue.main.async {
completion(nil,jsonErr)
}
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
}
You fundamentally misunderstand how async functions work. You function returns before the URLSession's dataTask has even begun to execute. A network request may take multiple seconds to complete. You ask it to fetch some data for you, give it a block of code to execute ONCE THE DATA HAS DOWNLOADED, and then go on with your business.
You can be certain that the line after the dataTask's resume() call will run before the new data has loaded.
You need to put code that you want to run when the data is available inside the data task's completion block. (Your statement print(currentWeatherUnwrapped) will run once the data has been read successfully.)
As you pointed out, the data ask is async, meaning you do not know when it will be completed.
One option is to modify your wrapper function getCurrentWeather to be async as well by not providing a return value, but instead a callback/closure. Then you will have to deal with the async nature somewhere else though.
The other option which is what you probably want in your scenario is to make the data task synchronous like so:
func getCurrentWeather () -> Bool {
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
let dispatchGroup = DispatchGroup() // <===
dispatchGroup.enter() // <===
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else {
dispatchGroup.leave() // <===
return
}
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
print(currentWeatherUnwrapped)
}
dispatchGroup.leave() // <===
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
dispatchGroup.leave() // <===
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
dispatchGroup.wait() // <===
return currentWeather != nil
}
The wait function can take parameters, which can define a timeout. https://developer.apple.com/documentation/dispatch/dispatchgroup Otherwise your app could be stuck waiting forever. You will then be able to define some action to present that to the user.
Btw I made a fully functional weather app just for learning, so check it out here on GitHub https://github.com/erikmartens/NearbyWeather. Hope the code there can help you for your project. It's also available on the app store.
EDIT: Please understand that this answer is meant to show how to make async calls synchronous. I am not saying this is good practice for handling network calls. This is a hacky solution for when you absolutely must have a return value from a function even though it uses async calls inside.
if let toID = message.chatPartnerId() {
firebaseReference.child(toID).observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
cell.nameLabel.text = dictionary["displayname"] as? String
let pic = dictionary["pictureURL"] as! String
print("THIS IS THE URL FOR EACH DISPLAYNAME")
print(dictionary["displayname"] as? String)
print(pic)
if let imageFromCache = MainPageVC.imageCache.object(forKey: pic as NSString) {
cell.pictureLabel.image = imageFromCache
} else {
let requested = URLRequest(url: URL(string: pic )!)
URLSession.shared.dataTask(with: requested) {data, response, err in
if err != nil {
print(err)
} else {
DispatchQueue.main.async {
let imageToCache = UIImage(data: data!)
MainPageVC.imageCache.setObject(imageToCache!, forKey: pic as NSString)
//cell.pictureLabel.image = nil
cell.pictureLabel.image = imageToCache
}
}
}.resume()
}
}
})
}
return cell
}
I'm running this code in my cellForRowAtIndexPath and I'm getting a ton of really bad behavior. I'm also getting similar behavior on other pages but for some reason this block of code with about a 90% consistency returns incorrect information for cells.
I get a lot of duplicate pictures being used, displaynames in the wrong places, but when I'm actually clicking into a person, my detail page shows the correct information every single time. That code is the typical didSelectRowAtIndexPath and passing the person.
What I don't understand is why on the initial load of this page all of the information is screwed up, but if I click into someone and come back the entire tableview has correct names and pictures. The names/pics also fix if I scroll a cell off the screen then come back to it.
I'm getting this behavior all over my app, meanwhile I see caching/loading done like this everywhere. Is it because I'm running the code in my cellForRowAtIndexPath? The only difference I see is that I'm running it there instead of creating a function inside of my Person class that configures cells and running it like that. What I don't understand is why that would make a difference because as far as I'm aware running a function within cellforRowAtIndexpath would be the same as copy-pasting that same code into there?
Any ideas/suggestions?
Edit: I'm getting a very similar situation when I'm running the following code:
self.PersonalSearchesList = self.PersonalSearchesList.sorted{ $0.users > $1.users }
self.tableView.reloadData()
Where I'm sorting my array before reloading my data. The information sometimes loads in incorrectly at first, but once I scroll the cell off the screen then come back to it it always corrects itself.
if you are using swift 3 here are some handy functions that allow you to save an image to your apps directory from an URL and then access it from anywhere in the app:
func saveCurrentUserImage(toDirectory urlString:String?) {
if urlString != nil {
let imgURL: URL = URL(string: urlString!)!
let request: URLRequest = URLRequest(url: imgURL)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {
(data, response, error) -> Void in
if (error == nil && data != nil) {
func display_image() {
let userImage = UIImage(data: data!)
if let userImageData = UIImagePNGRepresentation(userImage!) {
let filename = self.getDocumentsDirectory().appendingPathComponent("userImage")
try? userImageData.write(to: URL(fileURLWithPath: filename), options: [.atomic])
}
}
DispatchQueue.main.async(execute: display_image)
}
})
task.resume()
}
}
and then access it with any view controller using this:
extension UIViewController {
func getImage(withName name: String) -> UIImage {
let readPath = getDocumentsDirectory().appendingPathComponent(name)
let image = UIImage(contentsOfFile: readPath)
return image!
}
}
and finally calling it like this:
cell.pictureLabel.image = getImage(withName: "userImage")
If you can run the saveCurrentUserImage function prior to running cellForRowAtIndexPath then you can just check if the photo is nil in the directory before attempting to download it. You might be getting funny behavior when the page initially loads because you have multiple network calls going on at once. I wouldn't recommend making any network calls in cellForRowAtIndexPath because every time the cells are re-initialized it's going to make that network call for each cell.
Hope it helps!
EDIT: This method of image saving and retrieval is for images that you want to persist. If you want to erase them from memory you'll have to delete them from your directory.