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.
Related
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
I am trying to allow users of my app to be able to retrieve the iCloud sharing link to a record that they have ALREADY shared. I can get the URL when creating the share by using the let share = CKShare(rootRecord: CKRecord) followed by Apple's UISharingController. However, every time I use this method, the old link becomes invalid and kicks others out of the share they have already joined. How can I allow the users (the record owners) to fetch a shareable URL for the records CKShare object at any time?
//Initially shares the record
let share = CKShare(rootRecord: newRecord)
share[CKShare.SystemFieldKey.title] = "Group" as CKRecordValue?
let modifyRecordsOperation = CKModifyRecordsOperation( recordsToSave: [newRecord, share], recordIDsToDelete: nil)
//Activates the UISharingController (code not shown)
Assuming the record has already been shared, you can get the URL to invite additional participants with the following:
if let shareReference = record.share {
database.fetch(withRecordID: shareReference.recordID) { (share, error) in
let shareURL = (share as? CKShare)?.url
}
}
If you wanted to show the UICloudSharingController again for an existing share (without it generating a new URL), you could do:
let sharingController = UICloudSharingController { (_, prepareCompletionHandler) in
func createNewShare() {
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
let share = CKShare(rootRecord: record)
share[CKShare.SystemFieldKey.title] = "Title" as CKRecordValue
share.publicPermission = .readWrite
let modifyRecordsOp = CKModifyRecordsOperation(recordsToSave: [share, record], recordIDsToDelete: nil)
modifyRecordsOp.modifyRecordsCompletionBlock = { records, recordIDs, error in
prepareCompletionHandler(share, self, error)
}
modifyRecordsOp.database = database
operationQueue.addOperation(modifyRecordsOp)
}
if let shareReference = record.share {
database.fetch(withRecordID: shareReference.recordID) { (share, error) in
guard let share = share as? CKShare else {
createNewShare()
return
}
prepareCompletionHandler(share, self, nil)
}
} else {
createNewShare()
}
}
It sounds like you've got multiple issues going on here. I'd recommend going through the UICloudSharingController documentation carefully.
The first issue is in the code you've provided you're creating a new share each time, when what you want to do is to create it once and then show that share any time you want to interact with it (including getting the share URL). After you create your share you'll probably want to persist that on device somehow (it can be as simple as storing it in UserDefaults).
The second issue is it sounds like you're not instantiating the UICloudSharingController correctly. When you initially create the CKShare you must use the UICloudSharingController init(preparationHandler:) initializer and create the share INSIDE of that.
As per the documentation for the different initializers (emphasis mine):
Important
You must initialize the controller with the correct initializer
method. Do not use init(preparationHandler:) if the CKRecord is
already shared. Likewise, do not use init(share:container:) if the
CKRecord is not shared. Using the wrong initializer leads to errors
when saving the record.
And when you then want to show the shareSheet, you use the UICloudSharingController init(share:container:) initializer, and feed it the share you have persisted to device.
Here's the code provided in the UICloudSharingController documentation for creating the CKShare:
func presentCloudSharingController(_ sender: Any) { guard
let barButtonItem = sender as? UIBarButtonItem,
let rootRecord = self.recordToShare else {
return
}
let cloudSharingController = UICloudSharingController { [weak self] (controller, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
guard let `self` = self else {
return
}
self.share(rootRecord: rootRecord, completion: completion)
}
if let popover = cloudSharingController.popoverPresentationController {
popover.barButtonItem = barButtonItem
}
self.present(cloudSharingController, animated: true) {}
}
func share(rootRecord: CKRecord, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) {
let shareRecord = CKShare(rootRecord: rootRecord)
let recordsToSave = [rootRecord, shareRecord];
let container = CKContainer.default()
let privateDatabase = container.privateCloudDatabase
let operation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: [])
operation.perRecordCompletionBlock = { (record, error) in
if let error = error {
print("CloudKit error: \(error.localizedDescription)")
}
}
operation.modifyRecordsCompletionBlock = { (savedRecords, deletedRecordIDs, error) in
if let error = error {
completion(nil, nil, error)
} else {
completion(shareRecord, container, nil)
}
}
privateDatabase.add(operation)
}
And next time you want to show it, it's much more strait forward:
let share = // yourCKShare
let container = // yourCKContainer
let viewController = // yourViewController
let shareController = UICloudSharingController(share: share, container: container)
viewController.present(shareController, animated: true)
And finally to answer the URL part of your question, the UICloudSharingController is used for all things sharing - while the controller is presenting the share, there's a "Copy Link" button available to the owner (and possibly the user as well if you allow public access - I haven't looked at that as my stuff is private):
I know this type of question has been asked 1e7 times but I have come across a specific issue that I don't think has been covered/is blatantly obvious but I am too novice to fix it on my own.
I have the following code snippet within my cellForRowAt method in a TableViewController:
let currentDictionary = parser.parsedData[indexPath.row] as Dictionary<String,String>
let urlString = currentDictionary["media:content"]
if urlString != nil {
let url = NSURL(string: urlString!)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url! as URL) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
DispatchQueue.main.async {
cell.thumbnailImageView.image = UIImage(data: data!)
}
}
}
Which executes fine, downloads the images and assigns them to the UIImageView of each tableViewCell.
There is a finite delay when scrolling the table as the images are downloaded 'on the fly' so to speak.
What I want to do is pre-download all these images and save them in a data structure so they are fetched from URL's less frequently.
I have tried the following implementation:
var thumbnail = UIImage()
for item in parser.parsedData {
let currentDictionary = item as Dictionary<String,String>
let title = currentDictionary["title"]
let link = currentDictionary["link"]
let urlString = currentDictionary["media:content"]
let url = NSURL(string: urlString!)
if urlString != nil {
let url = NSURL(string: urlString!)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url! as URL)
DispatchQueue.main.sync {
thumbnail = UIImage(data: data!)!
}
}
}
var newsArticle: News!
newsArticle = News(title: title!, link: link!, thumbnail: thumbnail)
news.append(newsArticle)
Where news is my data structure. This code also executes fine, however each thumbnail is a 0x0 sized image, size {0, 0} orientation 0 scale 1.000000, according to the console output.
Does anyone have any ideas how to download these images but not immediately assign them to a UIImageView, rather store them for later use?
The problem is that you create your newsArticle before the global dispatch queue even started to process your url. Therefore, thumbnail is still the empty UIImage() created in the very first line.
You'll have to create the thumbnail inside the inner dispatch closure, like:
for item in parser.parsedData {
guard let currentDictionary = item as? Dictionary<String,String> else { continue /* or some error handling */ }
guard let title = currentDictionary["title"] else { continue /* or some error handling */ }
guard let link = currentDictionary["link"] else { continue /* or some error handling */ }
guard let urlString = currentDictionary["media:content"] else { continue /* or some error handling */ }
guard let url = URL(string: urlString) else { continue /* or some error handling */ }
DispatchQueue.global().async {
if let data = try? Data(contentsOf: url) {
DispatchQueue.main.sync {
if let thumbnail = UIImage(data: data) {
let newsArticle = News(title: title, link: link, thumbnail: thumbnail)
news.append(newsArticle)
}
}
}
}
}
By the way, your very first code (cellForRow...) is also broken: You must not reference the cell inside the dispatch closure:
DispatchQueue.main.async {
// Never do this
cell.thumbnailImageView.image = UIImage(data: data!)
}
Instead, reference the IndexPath, retrieve the cell inside the clousure, and go on with that cell. But as you already mentioned, there are many many entries on stackoverflow regarding this issue.
In my application, button tapping downloads data from an Internet site. The site is a list of links containing binary data. Sometimes, the first link may not contain the proper data. In this case, the application takes the next link in the array and gets data from there. The links are correct.
The problem I have is that frequently (not always though) the application freezes for seconds when I tap on the button. After 5-30 seconds, it unfreezes and downloading implements normally. I understand, something is blocking the main thread. When stopping the process in xCode, I get this (semaphore_wait_trap noted):
This is how I do it:
// Button Action
#IBAction func downloadWindNoaa(_ sender: UIButton)
{
// Starts activity indicator
startActivityIndicator()
// Starts downloading and processing data
// Either use this
DispatchQueue.global(qos: .default).async
{
DispatchQueue.main.async
{
self.downloadWindsAloftData()
}
}
// Or this - no difference.
//downloadWindsAloftData()
}
}
func downloadWindsAloftData()
{
// Creates a list of website addresses to request data: CHECKED.
self.listOfLinks = makeGribWebAddress()
// Extract and save the data
saveGribFile()
}
// This downloads the data and saves it in a required format. I suspect, this is the culprit
func saveGribFile()
{
// Check if the links have been created
if (!self.listOfLinks.isEmpty)
{
/// Instance of OperationQueue
queue = OperationQueue()
// Convert array of Strings to array of URL links
let urls = self.listOfLinks.map { URL(string: $0)! }
guard self.urlIndex != urls.count else
{
NSLog("report failure")
return
}
// Current link
let url = urls[self.urlIndex]
// Increment the url index
self.urlIndex += 1
// Add operation to the queue
queue.addOperation { () -> Void in
// Variables for Request, Queue, and Error
let request = URLRequest(url: url)
let session = URLSession.shared
// Array of bytes that will hold the data
var dataReceived = [UInt8]()
// Read data
let task = session.dataTask(with: request) {(data, response, error) -> Void in
if error != nil
{
print("Request transport error")
}
else
{
let response = response as! HTTPURLResponse
let data = data!
if response.statusCode == 200
{
//Converting data to String
dataReceived = [UInt8](data)
}
else
{
print("Request server-side error")
}
}
// Main thread
OperationQueue.main.addOperation(
{
// If downloaded data is less than 2 KB in size, repeat the operation
if dataReceived.count <= 2000
{
self.saveGribFile()
}
else
{
self.setWindsAloftDataFromGrib(gribData: dataReceived)
// Reset the URL Index back to 0
self.urlIndex = 0
}
}
)
}
task.resume()
}
}
}
// Processing data further
func setWindsAloftDataFromGrib(gribData: [UInt8])
{
// Stops spinning activity indicator
stopActivityIndicator()
// Other code to process data...
}
// Makes Web Address
let GRIB_URL = "http://xxxxxxxxxx"
func makeGribWebAddress() -> [String]
{
var finalResult = [String]()
// Main address site
let address1 = "http://xxxxxxxx"
// Address part with type of data
let address2 = "file=gfs.t";
let address4 = "z.pgrb2.1p00.anl&lev_250_mb=on&lev_450_mb=on&lev_700_mb=on&var_TMP=on&var_UGRD=on&var_VGRD=on"
let leftlon = "0"
let rightlon = "359"
let toplat = "90"
let bottomlat = "-90"
// Address part with coordinates
let address5 = "&leftlon="+leftlon+"&rightlon="+rightlon+"&toplat="+toplat+"&bottomlat="+bottomlat
// Vector that includes all Grib files available for download
let listOfFiles = readWebToString()
if (!listOfFiles.isEmpty)
{
for i in 0..<listOfFiles.count
{
// Part of the link that includes the file
let address6 = "&dir=%2F"+listOfFiles[i]
// Extract time: last 2 characters
let address3 = listOfFiles[i].substring(from:listOfFiles[i].index(listOfFiles[i].endIndex, offsetBy: -2))
// Make the link
let addressFull = (address1 + address2 + address3 + address4 + address5 + address6).trimmingCharacters(in: .whitespacesAndNewlines)
finalResult.append(addressFull)
}
}
return finalResult;
}
func readWebToString() -> [String]
{
// Final array to return
var finalResult = [String]()
guard let dataURL = NSURL(string: self.GRIB_URL)
else
{
print("IGAGribReader error: No URL identified")
return []
}
do
{
// Get contents of the page
let contents = try String(contentsOf: dataURL as URL)
// Regular expression
let expression : String = ">gfs\\.\\d+<"
let range = NSRange(location: 0, length: contents.characters.count)
do
{
// Match the URL content with regex expression
let regex = try NSRegularExpression(pattern: expression, options: NSRegularExpression.Options.caseInsensitive)
let contentsNS = contents as NSString
let matches = regex.matches(in: contents, options: [], range: range)
for match in matches
{
for i in 0..<match.numberOfRanges
{
let resultingNS = contentsNS.substring(with: (match.rangeAt(i))) as String
finalResult.append(resultingNS)
}
}
// Remove "<" and ">" from the strings
if (!finalResult.isEmpty)
{
for i in 0..<finalResult.count
{
finalResult[i].remove(at: finalResult[i].startIndex)
finalResult[i].remove(at: finalResult[i].index(before: finalResult[i].endIndex))
}
}
}
catch
{
print("IGAGribReader error: No regex match")
}
}
catch
{
print("IGAGribReader error: URL content is not read")
}
return finalResult;
}
I have been trying to fix it for the past several weeks but in vain. Any help would be much appreciated!
let contents = try String(contentsOf: dataURL as URL)
You are calling String(contentsOf: url) on the main thread (main queue). This downloads the content of the URL into a string synchronously The main thread is used to drive the UI, running synchronous network code is going to freeze the UI. This is a big no-no.
You should never call readWebToString() in the main queue. Doing DispatchQueue.main.async { self.downloadWindsAloftData() } exactly put the block in the main queue which we should avoid. (async just means "execute this later", it is still executed on Dispatch.main.)
You should just run downloadWindsAloftData in the global queue instead of main queue
DispatchQueue.global(qos: .default).async {
self.downloadWindsAloftData()
}
Only run DispatchQueue.main.async when you want to update the UI.
Your stack trace is telling you that it's stopping at String(contentsOf:), called by readWebToString, called by makeGribWebAddress.
The problem is that String(contentsOf:) performs a synchronous network request. If that request takes any time, it will block that thread. And if you call this from the main thread, your app may freeze.
Theoretically, you could just dispatch that process to a background queue, but that merely hides the deeper problem, that you are doing a network request with an API that is synchronous, non-cancellable, and offers no meaningful error reporting.
You really should doing asynchronous requests with URLSession, like you have elsewhere. Avoid using String(contentsOf:) with remote URL.
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.