No Data in Array after appending using Swift and xcode - ios

I am trying to download images from a url and then save them in an array of NSData.
I have a Class called Data Manager in which all my data is stored as well as functions for downloading images and getting data from URL.
In the same class I declare a variable called imageData of type [NSData] and let it equal an empty array as follows:
var imageData: [NSData] = []
here is what my other 2 functions look like:
func getDataFromUrl(url:NSURL, completion: ((data: NSData?, response: NSURLResponse?, error: NSError? ) -> Void)) {
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) in
completion(data: data, response: response, error: error)
}.resume()
}
func downloadImage(url: NSURL){
print("Download Started")
print("lastPathComponent: " + (url.lastPathComponent ?? ""))
getDataFromUrl(url) { (data, response, error) in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
guard let data = data where error == nil else { return }
print(response?.suggestedFilename ?? "")
print("Download Finished")
self.imageData.append(data)
print("you have \(self.imageData.count)")
}
print("you still do have \(self.imageData.count)")
}
}
I call these functions in my app Delegate class under the function didFinishLaunchingWithOptions as so
let dataManager = DataManager()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
dataManager.URLStringArray.removeAll()
for url in dataManager.objects.imageURLS {
dataManager.URLStringArray.append(url)
}
for url in dataManager.URLStringArray {
dataManager.downloadImage(NSURL(string: url)!)
print(url)
}
return true
}
In my view controller I go to get the data in the image array via following function:
func returnImageData() -> [NSData] {
print("your image count is \(imageData.count))")
return imageData
}
but the array is empty! Even though through the whole process I noticed that the array was becoming larger and larger because the print to the logs were showing the array increasing!
Thanks!

Since you are using async call to download the image data, at the the time you are printing the count of the imageData the image is not yet downloaded and so the array is not yet populated. Of course this is assuming that you are using the right property names as Eric.D has pointed out.

Related

How to handle errors while saving multiple images to the user's device?

My app as the functionality of choosing multiple images from the app main screen, and save the selected images to the user gallery.
As an example (image from google):
After the user clicking "save" I am doing the following in order to save the chosen images to the user's gallery.
Running through all of the images and saving each image that on clicked.
func saveSelectedImagesToDevice() {
for imageList in imagesListCells {
for image in imageList.images {
if image.selectionState == .onClicked {
downloadImage(from: image.url)
}
}
}
}
Downloading each image
func downloadImage(from url: String) {
guard let url = URL(string: url) else {return}
getData(from: url) { data, response, error in
guard let data = data, error == nil else { return }
guard let image = UIImage(data: data) else {return}
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
}
}
private func getData(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
#objc func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let _ = error {
self.delegate?.savedImage(proccessMsg: "Error adding images to the gallery, pleast make sure you enabled photos permissions in phone setting")
}
}
The thing is, because the saving process is asynchronies, in case error occurs in one of the process of downloading an image, I would like to stop all of the other asynchronies processes that running on the background.
At the moment, in case of error, the error been called for each one of the images.
Any ideas how can I manage it different in order to keep the process asynchronies but to be able to stop all processes in case of error?
You would have to change completely the architecture of the download to make it cancellable. A data task is cancellable, but yours is not because you have not retained any way of referencing it.
Apple suggests to not using the shared instance if you want to create multiple sessions. You could try to achieve this by creating a single session instance and invalidate it as soon as you receive an error.
Keep in mind that if you want to re-start the session you need to re instantiate a new one.
e.g.
let session = URLSession(configuration: .default)
func downloadImage(from url: String) {
guard let url = URL(string: url) else {return}
session.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
if let error = error {
print("You have an error: ",error.localizedDescription)
self.session.invalidateAndCancel()
}else if let data = data,
let image = UIImage(data: data) {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
}
}.resume()
}

Having Trouble With Completion Handlers and Closures in Swift

Background
The function below calls two functions, which both access an API, retrieve JSON data, parse through it, etc, and then take that data and populates the values of an object variable in my View Controller class.
func requestWordFromOxfordAPI(word: String, completion: (_ success: Bool) -> Void) {
oxfordAPIManager.fetchDictData(word: word)
oxfordAPIManager.fetchThesData(word: word)
completion(true)
}
Normally, if there was only one function fetching data, and I wanted to call a new function that takes in the data results and does something with them, I would use a delegate method and call it within the closure of the data fetching function.
For Example:
Here, I fetch data from my firebase database and if retrieving the data is succesful, I call self.delegate?.populateWordDataFromFB(result: combinedModel). Since closures occur on separate thread, this ensures that the populateWordDataFromFB function runs only once retrieving the data has finished. Please correct me if I am wrong. I have just recently learned this and am still trying to see the whole picture.
func readData(word: String) {
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
docRef.getDocument { (document, error) in
let result = Result {
try document.flatMap {
try $0.data(as: CombinedModel.self)
}
}
switch result {
case .success(let combinedModel):
if let combinedModel = combinedModel {
self.delegate?.populateWordDataFromFB(result: combinedModel)
} else {
self.delegate?.fbDidFailWithError(error: nil, summary: "\(word) not found, requesting from OxfordAPI")
self.delegate?.requestWordFromOxfordAPI(word: word, completion: { (success) in
if success {
self.delegate?.populateWordDataFromOX()
} else {print("error with completion handler")}
})
}
case .failure(let error):
self.delegate?.fbDidFailWithError(error: error, summary: "Error decoding CombinedModel")
}
}
}
Also notice from the above code that if the data is not in firebase, I call the delegate method below, which is where I am running into my issue.
self.delegate?.requestWordFromOxfordAPI(word: word, completion: { (success) in
if success {
self.delegate?.populateWordDataFromOX()
} else {print("error with completion handler")}
})
My Issue
What I am struggling with is the fact that the oxfordAPIManager.fetchDictData(word: word) and oxfordAPIManager.fetchThesData(word: word) functions both have closures.
The body of these functions look like this:
if let url = URL(string: urlString) {
var request = URLRequest(url: url)
request.addValue(K.APISettings.acceptField, forHTTPHeaderField: "Accept")
request.addValue(K.APISettings.paidAppID , forHTTPHeaderField: "app_id")
request.addValue(K.APISettings.paidAppKey, forHTTPHeaderField: "app_key")
let session = URLSession.shared
_ = session.dataTask(with:request) { (data, response, error) in
if error != nil {
self.delegate?.apiDidFailWithError(error: error, summary: "Error performing task:")
return
}
if let safeData = data {
if let thesaurusModel = self.parseThesJSON(safeData) {
self.delegate?.populateThesData(thesModel: thesaurusModel, word: word)
}
}
}
.resume()
} else {print("Error creating thesaurus request")}
I assume both of these functions are running on separate threads in the background. My goal is to call another function once both the oxfordAPIManager.fetchDictData(word: word) and oxfordAPIManager.fetchThesData(word: word) functions run. These two functions will populate the values of an object variable in my view controller which I will use in the new function. I don't want the new function to be called before the object variable in the view controller is populated with the right data so I tried to implement a completion handler. The completion handler function is being called BEFORE the two functions terminate, so when the new function tries to access the object variable in the View Controller, it's empty.
This is my first time trying to implement a completion handler and I tried to follow some other stack overflow posts but was unsuccessful. Also if this is the wrong approach let me know too, please. Sorry for the long explanation and thank you for any input.
Use DispatchGroup for this,
Example:
Create a DispatchGroup,
let group = DispatchGroup()
Modify the requestWordFromOxfordAPI(word: completion:) method to,
func requestWordFromOxfordAPI(word: String, completion: #escaping (_ success: Bool) -> Void) {
fetchDictData(word: "")
fetchThesData(word: "")
group.notify(queue: .main) {
//code after both methods are executed
print("Both methods executed")
completion(true)
}
}
Call enter() and leave() methods of DispatchGroup at the relevant places in fetchDictData(word:) and fetchThesData(word:) methods.
func fetchDictData(word: String) {
group.enter()
URLSession.shared.dataTask(with: url) { (data, response, error) in
//your code
group.leave()
}.resume()
}
func fetchThesData(word: String) {
group.enter()
URLSession.shared.dataTask(with: url) { (data, response, error) in
//your code
group.leave()
}.resume()
}
At last call requestWordFromOxfordAPI(word: completion:)
requestWordFromOxfordAPI(word: "") { (success) in
print(success)
}

Dynamic link object has no url

I created the DynamicLink for my firebase project when I am trying to receive the link I am getting "That's weird. My dynamic link object has no url".
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if let incomingURL = userActivity.webpageURL{
print("Incoming URL is \(incomingURL)")
let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL)
{(dynamicLink, error) in
guard error == nil else{
print("Found an error! \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink{
self.handleIncomingDynamicLink(dynamicLink)
}
}
if linkHandled{
return true
}
else{
return false
}
}
return false
}
func handleIncomingDynamicLink(_ dynamicLink: DynamicLink){
guard let url = dynamicLink.url else{
print("That's weird. My dynamic link object has no url")
return
}
print("Your incoming link parameter is\(url.absoluteString)")
}
After checking all the blogs and posted this issue on firebase, I didn't find any solution for this but I came up with this concrete solution and it will work definitely
here: dynamicLinkURL is your main dynamic link and shortHandURL is your deeplink URL which is associated with your dynamic link. I hope the below snippet will help you.
func dynamicLinkhandler(_ dynamicLinkURL: URL, onCompletion: #escaping(_ success: Bool) -> Void) {
URLSession.shared.dataTask(with: dynamicLinkURL) { (data, response, error) in
guard error == nil else {
print("Found Error \(String(describing: error?.localizedDescription)))")
return
}
guard let shortHandURL = response?.url, shortHandURL != dynamicLinkURL else {
print("Thats Weird, my dynamic link has no URL")
onCompletion(false)
return
}
onCompletion(true)
}.resume()
}
Double check that the bundle id you set up in the dynamic link wizard creation within the firebase console it's the one you are running the app into.
I have three different bundle ids (dev, enterprise, production) and, for instance, if a set in the link the production bundle id but the app runs the dev bundle id, instead of returning back some error it returned an honest dynamicLink object but with a nil value in the url.

Correct use of background fetch completion handler

My app uses CloudKit and I am trying to implement background fetch.
The method in App Delegate calls a method in my main view controller which checks for changes in the CloudKit database.
However, I realise that I am not calling the completion handler correctly, as the closures for the CloudKit will return asynchronously. I am really unsure how best to call the completion handler in the app delegate method once the operation is complete. Can I pass the completion handler through to the view controller method?
App Delegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
// Code to get a reference to main view controller
destinationViewController.getZoneChanges()
completionHandler(.newData)
}
}
Main view controller method to get CloudKit changes
// Fetch zone changes (a method in main table view controller)
func getZoneChanges() {
DispatchQueue.global(qos: .userInitiated).async {
let customZone = CKRecordZone(zoneName: "Drugs")
let zoneID = customZone.zoneID
let zoneIDs = [zoneID]
let changeToken = UserDefaults.standard.serverChangeToken // Custom way of accessing User Defaults using an extension
// Look up the previous change token for each zone
var optionsByRecordZoneID = [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneOptions]()
// Some other functioning code to process options
// CK Zone Changes Operation
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zoneIDs, optionsByRecordZoneID: optionsByRecordZoneID)
// Closures for records changed, deleted etc.
// Closure details omitted for brevity as fully functional as expected.
// These closures change data model, Spotlight indexing, notifications and trigger UI refresh etc.
operation.recordChangedBlock = { (record) in
// Code...
}
operation.recordWithIDWasDeletedBlock = { (recordId, string) in
// Code...
}
operation.recordZoneChangeTokensUpdatedBlock = { (zoneId, token, data) in
UserDefaults.standard.serverChangeToken = changeToken
UserDefaults.standard.synchronize()
}
operation.recordZoneFetchCompletionBlock = { (zoneId, changeToken, _, _, error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
}
UserDefaults.standard.serverChangeToken = changeToken
UserDefaults.standard.synchronize()
}
operation.fetchRecordZoneChangesCompletionBlock = { (error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
} else {
print("Changes fetched successfully!")
// Save local items
self.saveData() // Uses NSCoding
}
}
CKContainer.default().privateCloudDatabase.add(operation)
}
}
Update your getZoneChanges to have a completion parameter.
func getZoneChanges(completion: #escaping (Bool) -> Void) {
// the rest of your code
operation.fetchRecordZoneChangesCompletionBlock = { (error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
completion(false)
} else {
print("Changes fetched successfully!")
completion(true)
}
}
}
Then you can update the app delegate method to use it:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
// Code to get a reference to main view controller
destinationViewController.getZoneChanges { (success) in
completionHandler(success ? .newData : .noData)
}
}
}

URLSession cached data

I'm new in iOS dev and I do not understand one think. So I have gz file and inside gzip there is xml file. I need to download gz file, every time user start the app. First time when I start my app I use this code to get data. Problem is my xml file was offline on server for few day and my app always start without problem and show data with no problem. So all my files was cached on device?. I want my data is retrieved every time while the user start the application. I am not sure do I did something wrong? Thanks
let url = NSURL(string: "http://sitename/xxx.gz")
if url != nil {
let task = URLSession.shared.dataTask(with: url! as URL, completionHandler: { (data, response, error) -> Void in
if error == nil {
let nsdata = data as NSData?
let content = nsdata?.gunzipped()
let dataContent = content as Data?
let urlContent = NSString(data: dataContent!, encoding: String.Encoding.ascii.rawValue) as NSString!
let xml = XMLParser()
xml.getDataforTable(data: urlContent as! String)
NotificationCenter.default.post(Notification(name: Notification.Name(rawValue: "XmlDataLoaded"), object: nil))
} else {
NotificationCenter.default.post(Notification(name: Notification.Name(rawValue: "DataNotLoaded"), object: nil))
}
})
task.resume()
}
enter code here
in AppDelegate.swift
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
call your web services method inside this method
}

Resources