URLSession dataTask execution order - ios

i am trying to fetch images data using URLSession dataTask the urls are fetched from a firebase firestore document that contains each download path using for loop in snapShotDocuments in ascending order, after that the urls are passed into the URLSession dataTask that retrieves the data then appending the result in an array tableCells[] to update a tableview, the problem is the order of the cells in the updated tableview is not the same order of the objects in tableCells array, i am expecting it has something to do with concurrency that i am not aware of here is my code
public func fetchCells() {
guard (UserDefaults.standard.value(forKeyPath: "email") as? String) != nil else {
return
}
spinner.textLabel.text = "Loading"
spinner.position = .center
spinner.show(in: tableView)
db.collection("ads").order(by: "timeStamp").addSnapshotListener { snapshot, error in
self.tableCells = []
guard error == nil , let snapShotDocuments = snapshot?.documents else {
return
}
guard !snapShotDocuments.isEmpty else {
print("snapshot is empty ")
DispatchQueue.main.async {
self.tableView.isHidden = true
self.spinner.dismiss()
}
return
}
for i in snapShotDocuments {
let documentData = i.data()
guard let imageURL = documentData["imageurl"] as? String , let imageStringURL = URL(string: imageURL) else {
print("no url ")
return
}
guard let descriptionLabel = documentData["adDescription"] as? String , let titleLabel = documentData["adTitle"] as? String , let timeStamp = documentData["timeStamp"] as? Double else {
print("error")
return
}
URLSession.shared.dataTask(with: imageStringURL) { data , _ , error in
guard error == nil , let data = data else {
return
}
let image = UIImage(data: data)
let newCell = adoptionCell(cellImage: image, descriptionLabel: descriptionLabel, titleLabel: titleLabel, timePosted: timeStamp, imageUrl: nil)
self.tableCells.append(newCell)
DispatchQueue.main.async {
self.tableView.reloadData()
self.spinner.dismiss()
}
}.resume()
}
}
}

yes correct some image might be loaded faster another is loaded slower. therefore position in final array is changed.
I would rather access tableCells in main thread. here I reload cells in batch. index is used for setting position of the cell in final array.
var tableCells = Array<TableCell?>(repeating: nil, count: snapShotDocuments.count) //preserve space for cells...
var count: Int32 = 0 // actual number of real load tasks
for tuple in snapShotDocuments.enumerated() {
let i = tuple.element
let index = tuple.offset //offset of cell in final array.
let documentData = i.data()
guard let imageURL = documentData["imageurl"] as? String , let imageStringURL = URL(string: imageURL) else {
print("no url ")
return
}
guard let descriptionLabel = documentData["adDescription"] as? String , let titleLabel = documentData["adTitle"] as? String , let timeStamp = documentData["timeStamp"] as? Double else {
print("error")
return
}
count += 1 //increment count as there is new task..
URLSession.shared.dataTask(with: imageStringURL) { data , _ , error in
if error == nil, let data = data {
let image = UIImage(data: data)
let newCell = adoptionCell(cellImage: image, descriptionLabel: descriptionLabel, titleLabel: titleLabel, timePosted: timeStamp, imageUrl: nil)
//self.tableCells.append(newCell)
tableCells[index] = newCell //because array has predefined capacity, thread safe...
}
guard OSAtomicDecrement32(&count) == 0 else { return }
//last task, then batch reload..
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.tableCells = tableCells.compactMap { $0 }
self.tableView.reloadData()
self.spinner.dismiss()
}
}.resume()
}

What you have:
for i in snapShotDocuments {
dataTask {
mutate tableCells (append) on background thread <- don't do that, A) not thread safe, and B) append won't happen in order they were dispatched, but the order they came back
dispatch back to main {
reload data <- don't do that, reload the individual rows if needed, or reload everything at the end
}
}
You're enqueuing a number of asynchronous operations that can take varying amount of time to complete. Enqueue them in order 1, 2, 3, 4 and they could come back in order 3, 1, 4, 2, for example.
What you want:
Your model, arranged data instances, let's say an array, of structs, not UITableViewCell's.
for i in snapShotDocuments {
dataTask {
process on background thread, but then
dispatch back to main {
look up in the model, the object for which we have the new data
mutate the model array
then reload row at index path for the row involved
}
}

Related

TableView with labels, images, gifs and video hangs / gets stuck incorrect while fetch from firestore in iOS, Swift

I have tableview with label, imageView (for image, gif & video thumbnail). I am sure that doing something wrong and I can't handle its completion handler due to which the app is hanged and gets stuck for a long time.
My model is like,
struct PostiisCollection {
var id :String?
var userID: String?
var leadDetails : NSDictionary?
var company: NSDictionary?
var content: String?
init(Doc: DocumentSnapshot) {
self.id = Doc.documentID
self.userID = Doc.get("userID") as? String ?? ""
self.leadDetails = Doc.get("postiiDetails") as? NSDictionary
self.company = Doc.get("company") as? NSDictionary
self.content = Doc.get("content") as? String ?? ""
}
}
I wrote in my view controller for fetch this,
var postiisCollectionDetails = [PostiisCollection]()
override func viewDidLoad() {
super.viewDidLoad()
let docRef = Firestore.firestore().collection("PostiisCollection").whereField("accessType", isEqualTo: "all_access")
docRef.getDocuments { (querysnapshot, error) in
if let doc = querysnapshot?.documents, !doc.isEmpty {
print("Document is present.")
for document in querysnapshot!.documents {
_ = document.documentID
if let compCode = document.get("company") as? NSDictionary {
do {
let jsonData = try JSONSerialization.data(withJSONObject: compCode)
let companyPost: Company = try! JSONDecoder().decode(Company.self, from: jsonData)
if companyPost.companyCode == AuthService.instance.companyId ?? ""{
print(AuthService.instance.companyId ?? "")
if (document.get("postiiDetails") as? NSDictionary) != nil {
let commentItem = PostiisCollection(Doc: document)
self.postiisCollectionDetails.append(commentItem)
}
}
} catch {
print(error.localizedDescription)
}
DispatchQueue.main.async {
self.tableView.isHidden = false
self.tableView.reloadData()
}
}
}
}
}
}
I need to check for the index path with image view is either image or gif or video with different parameters, I tried with tableview delegate and datasource method by,
extension AllAccessPostiiVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postiisCollectionDetails.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AllAccessCell", for: indexPath)
let label1 = cell.viewWithTag(1) as? UILabel
let imagePointer = cell.viewWithTag(3) as? UIImageView
let getGif = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "gif") as? NSArray
let getPhoto = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "photo") as? NSArray
let getVideo = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "video") as? NSArray
label1?.text = "\(arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "title"))"
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
print(arrGif[0])
let gifURL : String = "\(arrGif[0])"
let imageURL = UIImage.gifImageWithURL(gifURL)
imagePointer?.image = imageURL
playButton?.isHidden = true
}
if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
print(arrPhoto[0])
let storageRef = Storage.storage().reference(forURL: arrPhoto[0])
storageRef.downloadURL(completion: { (url, error) in
do {
let data = try Data(contentsOf: url!)
let image = UIImage(data: data as Data)
DispatchQueue.main.async {
imagePointer?.image = image
playButton?.isHidden = true
}
} catch {
print(error)
}
})
}
if getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let videoURL = URL(string: arrVideo[0])
let asset = AVAsset(url:videoURL!)
if let videoThumbnail = asset.videoThumbnail{
SVProgressHUD.dismiss()
imagePointer?.image = videoThumbnail
playButton?.isHidden = false
}
}
}
}
If I run, the app hangs in this page and data load time is getting more, some cases the preview image is wrongly displayed and not able to handle its completion
As others have mentioned in the comments, you are better of not performing the background loading in cellFroRowAtIndexPath.
Instead, it's better practice to add a new method fetchData(), where you perform all the server interaction.
So for example:
// Add instance variables for fast access to data
private var images = [UIImage]()
private var thumbnails = [UIImage]()
private func fetchData(completion: ()->()) {
// Load storage URLs
var storageURLs = ...
// Load data from firebase
let storageRef = Storage.storage().reference(forURL: arrPhoto[0])
storageRef.downloadURL(completion: { (url, error) in
// Parse data and store resulting image in image array
...
// Call completion handler to indicate that loading has finished
completion()
})
}
Now you can call fetchData() whenever you would like to refresh data and call tableview.reloadData() within the completion handler. That of course must be done on the main thread.
This approach simplifies your cellForRowAtIndexPath method.
There you can just say:
imagePointer?.image = ...Correct image from image array...
Without any background loading.
I suggest using below lightweight extension for image downloading from URL
using NSCache
extension UIImageView {
func downloadImage(urlString: String, success: ((_ image: UIImage?) -> Void)? = nil, failure: ((String) -> Void)? = nil) {
let imageCache = NSCache<NSString, UIImage>()
DispatchQueue.main.async {[weak self] in
self?.image = nil
}
if let image = imageCache.object(forKey: urlString as NSString) {
DispatchQueue.main.async {[weak self] in
self?.image = image
}
success?(image)
} else {
guard let url = URL(string: urlString) else {
print("failed to create url")
return
}
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
// response received, now switch back to main queue
DispatchQueue.main.async {[weak self] in
if let error = error {
failure?(error.localizedDescription)
}
else if let data = data, let image = UIImage(data: data) {
imageCache.setObject(image, forKey: url.absoluteString as NSString)
self?.image = image
success?(image)
} else {
failure?("Image not available")
}
}
}
task.resume()
}
}
}
Usage:
let path = "https://i.stack.imgur.com/o5YNI.jpg"
let imageView = UIImageView() // your imageView, which will download image
imageView.downloadImage(urlString: path)
No need to put imageView.downloadImage(urlString: path) in mainQueue, its handled in extension
In your case:
You can implement following logic in cellForRowAt method
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
let urlString : String = "\(arrGif[0])"
let image = UIImage.gifImageWithURL(urlString)
imagePointer?.image = image
playButton?.isHidden = true
}
else if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
let urlString = Storage.storage().reference(forURL: arrPhoto[0])
imagePointer?.downloadImage(urlString: urlString)
playButton?.isHidden = true
}
elseif getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let urlString = arrVideo[0]
imagePointer?.downloadImage(urlString: urlString)
playButton?.isHidden = false
}
If you have one imageView to reload in tableView for photo, video and gif. then use one image array to store it prior before reloading. So that your main issue of hang or stuck will be resolved. Here the main issue is each time in table view cell collection data is being called and checked while scrolling.
Now I suggest to get all photo, gifs and video (thumbnail) as one single array prior to table view reload try this,
var cacheImages = [UIImage]()
private func fetchData(completionBlock: () -> ()) {
for (index, _) in postiisCollectionDetails.enumerated() {
let getGif = postiisCollectionDetails[index].leadDetails?.value(forKey: "gif") as? NSArray
let getPhoto = postiisCollectionDetails[index].leadDetails?.value(forKey: "photo") as? NSArray
let getVideo = postiisCollectionDetails[index].leadDetails?.value(forKey: "video") as? NSArray
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
let gifURL : String = "\(arrGif[0])"
self.randomList.append(gifURL)
/////---------------------------
let imageURL = UIImage.gifImageWithURL(gifURL)
self.cacheImages.append(imageURL!)
//////=================
}
else if getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let videoURL: String = "\(arrVideo[0])"
let videoUrl = URL(string: arrVideo[0])
let asset = AVAsset(url:videoUrl!)
if let videoThumbnail = asset.videoThumbnail{
////--------------
self.cacheImages.append(videoThumbnail)
//-----------
}
self.randomList.append(videoURL)
}else if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
let photoURL : String = "\(arrPhoto[0])"
/////---------------------------
let url = URL(string: photoURL)
let data = try? Data(contentsOf: url!)
if let imageData = data {
let image = UIImage(data: imageData)
if image != nil {
self.cacheImages.append(image!)
}
else {
let defaultImage: UIImage = UIImage(named:"edit-user-80")!
self.cacheImages.append(defaultImage)
}
}
//////=================
}
else {
//-----------------
let defaultImage: UIImage = UIImage(named:"edit-user-80")!
self.cacheImages.append(defaultImage)
//--------------------
}
}
completionBlock()
}
After getting all UIImage as array where loop is being called. Now you call this function inside your viewDidLoad. So after all values in images fetched then try to call tableView like this,
override func viewDidLoad() {
self.fetchData {
DispatchQueue.main.async
self.tableView.reloadData()
}
}
}
Now atlast, you may use SDWebImage or any other background image class or download method to call those in tableView cellforRow method,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// your cell idetifier & other stuffs
if getVideo != nil {
imagePointer?.image = cacheImages[indexPath.row]
playButton?.isHidden = false
}else {
imagePointer?.image = cacheImages[indexPath.row]
// or get photo with string via SdWebImage
// imagePointer?.sd_setImage(with: URL(string: photoURL), placeholderImage: UIImage(named: "edit-user-80"))
playButton?.isHidden = true
}
return cell
}
You're handling data in a totally wrong manner. Data(contentsOf: url!) - This is wrong. You should chache the images and should download it to directory. When you convert something into data it takes place into the memory(ram) and it is not good idea when playing with large files. You should use SDWebImage kind of library to set images to imageview.
Second thing if let videoThumbnail = asset.videoThumbnail - This is also wrong. Why you're creating assets and then getting thumbnail from it? You should have separate URL for the thumbnail image for your all videos in the response of the API and then again you can use SDWebImage to load that thumbnail.
You can use SDWebImage for gif as well.
Alternative of SDWebImage is Kingfisher. Just go through both libraries and use whatever suitable for you.

Firebase Storage async image download random order

I'm trying to download images from firebase storage to display in my collectionview cells, but the images keep appearing in random order in the cells. The cells each have a label that is retrieved from firebase storage (item1, item2 etc) which displays nicely in the correct cell every time. The images stored in firebase storage each have their storage url as a child to their respective item name in the firebase database.
I'm sucesfully able to retrieve each image url, and download all the images and display them in the cells correctly, it's just that they keep appearing in randomized order every time I open the app, so the image does not correspond with the item name label.
I realize i need to asyncronously download the images, so each image finishes loading in the correct cell before continuing to the next, but I'm having trouble doing so. Heres my code so far:
func downloadImg(completion: #escaping (UIImage?) -> ()) {
let ref = Database.database().reference().child("somePath")
ref.observeSingleEvent(of: .value) { (snapshot) in
for item in snapshot.children {
let snap = item as! DataSnapshot
let imageSnap = snap.childSnapshot(forPath: "img/storageUrl")
if let url = imageSnap.value as? String {
let someRef = self.storageRef.reference(forURL: url)
someRef.getData(maxSize: 10 * 10024 * 10024) { data, error in
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
DispatchQueue.main.async {
completion(image)
}
}
}
}
}
}
}
Then I call my function in the viewdidload:
downloadImg { (completion) in
self.itemPicArray.append(completion!)
self.collectionView?.reloadData()
}
Finally i set my cell imageview to itemPicArray[indexPath.row]
Like I said, this works perfectly except the fact that the images keep showing up randomly. Help very much appreciated!
Your problem is that everytime an image comes in, you reload the entire collection view. Depending on the sizes of the images and the state of the network, the images will come in in a different order almost every time.
Consider downloading all of the images first and then reloading the collection view once. If there are a lot of images, consider paginating your results. You can enumerate the loop and sort the data source array by this original order. I've added a custom data object to help with that.
class CustomObject {
var image: UIImage?
let n: Int
init(image: UIImage?, n: Int) {
self.image = image
self.n = n
}
}
let dispatch = DispatchGroup()
for (n, item) in snapshot.children.enumerated() {
let object = CustomObject(image: nil, n: n) // init custom object with n (image is still nil)
dispatch.enter() // enter dispatch
someRef.getData(maxSize: 10 * 10024 * 10024) { data, error in // download image
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
object.image = image // inject custom object with image
itemPicArray.append(object) // append to array
}
dispatch.leave() // leave dispatch
}
}
dispatch.notify(queue: .global()) { // dispatch completion
itemPicArray.sort { $0.n < $1.n } // sort by n (original download order)
collectionView.reloadData() // reload collection view
}
Using a model could be a good idea.
struct Image {
var imageName: String
var image: UIImage
}
This way, no matter the order, the item name (image name) and the image will be paired.
Perhaps a better solution now is to configure method downloadImg so that it takes the imageName as a parameter. Then you can call the correct node to get the corresponding storageURL.
func downloadImg(imageName: String, completion: #escaping (Image?) -> ()) {
// Use the parameter to create your database reference
let ref = Database.database().reference().child(imageName)
ref.observeSingleEvent(of: .value) { (snapshot) in
for item in snapshot.children {
let snap = item as! DataSnapshot
let imageSnap = snap.childSnapshot(forPath: "img/storageUrl")
if let url = imageSnap.value as? String {
let someRef = self.storageRef.reference(forURL: url)
someRef.getData(maxSize: 10 * 10024 * 10024) { data, error in
if let error = error {
print(error)
return
}
if let image = UIImage(data: data) {
// Create a variable of type Image (your custom model)
let imageWithName = Image(imageName: imageName, image: image)
completion(imageWithName)
}
}
}
}
}
}
Calling and handling could be done like so:
// Create a variable to hold your item name/image-pairs
var imagesWithNames = [Image]()
let dispatchGroup = DispatchGroup()
// Iterate over your array of item names
for item in itemArray {
dispatchGroup.enter()
downloadImg(item) { (imageWithName) in
self.imagesWithNames.append(imageWithName)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) { {
self.collectionView?.reloadData()
}
And to populate the collectionView you can go:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! YourCustomCell
// Get the pair at the given index
let imageWithName = self.imagesWithNames[indexPath.row]
DispatchQueue.main.async {
// Set image and item label (example below)
self.yourImageView.image = imageWithName.image
self.yourItemLabel.text = imageWithName.imageName
}
return cell
}
If anyone is having issues with this in the future, use -bsod answer and create a custom object. Also create a variable var counter: Int = 0. Here's how my code looks like Swift 5.X and it works perfectly.
class CustomObject {
var image: UIImage?
let n: Int
init(image: UIImage?, n: Int) {
self.image = image
self.n = n
}
}
func reloadStuff() {
dispatch.notify(queue: .main) {
self.imageArrayCells.sort { $0.n < $1.n }
self.contentViewProfile.collectionView.refreshControl?.endRefreshing()
self.contentViewProfile.collectionView.reloadData()
}
}
for i in 0 ..< self.imageArrayInfo.count {
let object = CustomObject(image: nil, n: i)
let urlString = self.imageArrayInfo[i]
print(object.n)
let url = URL(string: urlString)
self.dispatch.enter()
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
self.dispatch.enter()
guard let data = data, error == nil else {
return
}
guard let image = UIImage(data: data) else {
return
}
object.image = image
self.imageArrayCells.append(object)
self.dispatch.leave()
self.counter += 1
if self.counter == self.imageArrayInfo.count {
for i in 0 ..< self.imageArrayInfo.count {
self.dispatch.leave()
}
self.counter = 0
}
}
task.resume()
}
self.reloadStuff()
Here's what I'm calling in collectionView(cellForItemAt:_)
cell.imageInProfileCollection.image = imageArrayCells[indexPath.row].image

How to show all data in table view during pagination in swift 3?

Here i had implemented pagination for the table view and items are loaded by using model class but here the loaded items are replacing with the new items and whenever it calls api it returns the new data and old data is overriding on it and displaying only 10 items at a time i am implementing it for first time can anyone help me how to resolve the issue ?
func listCategoryDownloadJsonWithURL(listUrl: String) {
let url = URL(string: listUrl)!
print(listUrl)
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil { print(error!); return }
do {
if let jsonObj = try JSONSerialization.jsonObject(with: data!) as? [String:Any] {
self.listClassModel = ModelClass(dict: jsonObj as [String : AnyObject])
DispatchQueue.main.async {
guard let obj = self.listClassModel else { return }
let itemsCount = obj.items.count
print(itemsCount)
for i in 0..<itemsCount {
let customAttribute = obj.items[i].customAttribute
for j in 0..<customAttribute.count {
if customAttribute[j].attributeCode == "image" {
let baseUrl = "http://192.168.1.11/magento2/pub/media/catalog/product"
self.listCategoryImageArray.append(baseUrl + customAttribute[j].value)
print(self.listCategoryImageArray)
}
}
}
self.activityIndicator.stopAnimating()
self.activityIndicator.hidesWhenStopped = true
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.reloadData()
self.collectionView.isHidden = false
self.tableView.reloadData()
}
}
} catch {
print(error)
}
}
task.resume()
}
You are assigning your result data to model array, each time you call your API. This is the reason that your old data is getting replaced with new one. Rather than assigning, you should append the new data to your datasource array.
if let jsonObj = try JSONSerialization.jsonObject(with: data!) as? [String:Any] {
self.listClassModel.append(contentsOf: ModelClass(dict: jsonObj as [String : AnyObject]))
Also make sure you initialize your array as an empty array first. (maybe in declaration or viewDidLoad) before calling API.

How to insert a value into a URL to make a request to YQL

I'm running into a problem when I try to make a request to YQL for stock data, when the symbol (newCompanyStockSymbol) to look up is user-entered. I fetch the stocks in this function:
func handleSave() {
// Fetch stock price from symbol provided by user for new company
guard let newCompanyStockSymbol = stockTextField.text else {
print("error getting text from field")
return
}
var newCompanyStockPrice = ""
let url = URL(string: "https://query.yahooapis.com/v1/public/yql?q=select%20symbol%2C%20Ask%2C%20YearHigh%2C%20YearLow%20from%20yahoo.finance.quotes%20where%20symbol%20in%20(%22\(newCompanyStockSymbol)%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
let json = JSON(data: data!)
if let quotes = json["query"]["results"]["quote"].array {
for quote in quotes {
let ask = quote["Ask"].stringValue
newCompanyStockPrice = ask
}
}
print("new company json: \(json)")
}
guard let newCompanyName = self.nameTextField.text else {
print("error getting text from field")
return
}
guard let newCompanyLogo = self.logoTextField.text else {
print("error getting text from field")
return
}
print("2: The new commpany stock price is: \(newCompanyStockPrice)")
// Call save function in view controller to save new company to core data
self.viewController?.save(name: newCompanyName, logo: newCompanyLogo, stockPrice: newCompanyStockPrice)
self.viewController?.tableView.reloadData()
}
task.resume()
// Present reloaded view controller with new company added
let cc = UINavigationController()
let companyController = CompanyController()
viewController = companyController
cc.viewControllers = [companyController]
present(cc, animated: true, completion: nil)
}
And I use string interpolation to insert \(newCompanyStockSymbol) into the request URL at the appropriate place. However I get a crash and error on that line because it's returning nil, I expect because it's using the URL with \(newCompanyStockSymbol) in there verbatim, instead of actually inserting the value.
Is there another way to do this?
EDIT
And the save function in view controller that's called from handleSave() above if it's helpful:
func save(name: String, logo: String, stockPrice: String) {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
let entity =
NSEntityDescription.entity(forEntityName: "Company",
in: managedContext)!
let company = NSManagedObject(entity: entity,
insertInto: managedContext)
company.setValue(stockPrice, forKey: "stockPrice")
company.setValue(name, forKey: "name")
company.setValue(logo, forKey: "logo")
do {
try managedContext.save()
companies.append(company)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
tableView.reloadData()
}
Supposing you entered AAPL in your stockTextField, using simply:
let newCompanyStockSymbol = stockTextField.text
results in newCompanyStockSymbol being:
Optional("AAPL")
which is not what you want in your URL string. The critical section ends up like this:
(%22Optional("AAPL")%22)
Instead, use guard to get the value from the text field:
guard let newCompanyStockSymbol = stockTextField.text else {
// handle the error how you see fit
print("error getting text from field")
return
}
Now your URL should be parsed correctly.
--- Additional info ---
I'm not entirely sure of the rules on 'continued conversation' around here, but hopefully editing this will be acceptable... anyway...
Make sure you are following this flow:
func handleSave() {
let newCompanyName = nameTextField.text
let newCompanyStockSymbol = stockTextField.text
let newCompanyLogo = logoTextField.text
var newCompanyStockPrice = ""
// Fetch stock price from symbol provided by user for new company
let url = URL(string: "https://query.yahooapis.com/v1/public/yql?q=select%20symbol%2C%20Ask%2C%20YearHigh%2C%20YearLow%20from%20yahoo.finance.quotes%20where%20symbol%20in%20(%22\(newCompanyStockSymbol)%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
let json = JSON(data: data!)
if let quotes = json["query"]["results"]["quote"].array {
for quote in quotes {
let ask = quote["Ask"].stringValue
newCompanyStockPrice = ask
// task completed, we've parsed the return data,
// so NOW we can finish the save process and
// update the UI
viewController?.save(name: newCompanyName!, logo: newCompanyLogo!, stockPrice: newCompanyStockPrice)
}
}
}
}
task.resume()
}
I'm not testing this, so it might need a tweak, and your .save() function may need to be forced onto the main thread (since it's doing UI updates). But maybe that's a little more clear.

Getting data not according to the same order that I call those methods in swift

I have aded HMSegmentedControl to make a swiping segmented control in my iOS app.I am loading all the data initially because then it will facilitate the scrolling. So I have to load several tables under several categories. Category name is the segmented control item title. So this is how I set my titles.
for(var i=0; i<dm.TableData.count; i++)
{
self.array.append(dm.TableData[i]["name"] as! String)
}
segmentedControl.sectionTitles=self.array
Categories are loading according to the order of this array without any issue. Then I am loading my tables like this.
for i in 0..<dm.TableData.count {
self.catID=self.dm.TableData[i]["term_id"] as? String
switch self.catID {
case "55":
self.jsonParser()
case "1":
self.getBusiness()
case "2":
self.getNews()
case "4":
self.getSports()
case "5":
self.getEntertainment()
case "57":
self.getCrime()
case "21":
self.getPolitics()
case "28":
self.getWorld()
case "89":
self.getVideos()
case "111":
self.getLocalNews()
default:
print("Default")
}
}
This is my jsonParser method. getBusiness(),getNews(),getSports() all those methods are just same as this and load to seperate array and the dictionary key is different.
func jsonParser() {
let urlPath = "http://www.li67t8.lk/mobileapp/news.php?"
let category_id=self.catID
let catParam="category_id"
let strCatID="\(catParam)=\(category_id)"
let strStartRec:String=String(startRec)
let startRecPAram="start_record_index"
let strStartRecFull="\(startRecPAram)=\(strStartRec)"
let strNumOfRecFull="no_of_records=10"
let fullURL = "\(urlPath)\(strCatID)&\(strStartRecFull)&\(strNumOfRecFull)"
print(fullURL)
guard let endpoint = NSURL(string: fullURL) else {
print("Error creating endpoint")
return
}
let request = NSMutableURLRequest(URL:endpoint)
NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) in
do {
guard let data = data else {
throw JSONError.NoData
}
guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? NSDictionary else {
throw JSONError.ConversionFailed
}
print(json)
if let countries_list = json["data"] as? NSArray
{
// for (var i = 0; i < countries_list.count ; i++ )
for i in 0..<countries_list.count
{
if let country_obj = countries_list[i] as? NSDictionary
{
//self.TableData.append(country_obj)
self.breakingNews.append(country_obj)
}
}
dispatch_async(dispatch_get_main_queue()) {
print("%%%%%%%%%%% CAT ID %%%%%%%%%% \(self.catID)")
if let checkedUrl = NSURL(string: self.breakingNews[0]["thumb_url"] as! String) {
self.imageURL=checkedUrl
}
if let time = self.breakingNews[0]["duration"] as? String
{
self.timeDuration=time
}
if let likes = self.breakingNews[0]["read_count"] as? String
{
self.noOfLikes=likes
}
if let title = self.breakingNews[0]["post_title"] as? String
{
self.titleNews=title
}
self.addedArray.append("Breaking News")
self.commonData["Breaking News"]=self.breakingNews
self.updateUI()
print("-------BREAKING--------")
}
}
} catch let error as JSONError {
print(error.rawValue)
} catch let error as NSError {
print(error.debugDescription)
}
}.resume()
}
I have one method for UpdateUI() and it creates UITableView dynamically and assign tag value dynamically (I keep an Int called index and I assign that index to tableview tag and after adding table to super view I increment the index count by 1)
According to this I get data and load to the tableview. But my problem is data not getting in the same order I call to those methods. As an example jsonParser() returns its data set and then it returns getSportsData() data. like wise my data not according to the segment title order.
So how can I solve this problem? Please help me.
Thanks

Resources