Not Able to append the documents retrieved - ios

I am not able to append the documents retrieved from the Firestore database in chat application based on Swift IOS to the "messages" variable, after appending I have configure the table cells as below in the code, I am getting the following error
Error
Cannot convert value of type '[QueryDocumentSnapshot]' to expected argument type 'DocumentSnapshot'
Code
var messages: [DocumentSnapshot]! = []
func configuredatabase ()
{
db.collection("messages").document("hello").collection("newmessages").document("2").collection("hellos").document("K").collection("messages").addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
//here is the error
self.messages.append(documents)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Dequeue cell
let cell = self.clientTable .dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
// Unpack message from Firebase DataSnapshot
let messageSnapshot = self.messages![indexPath.row]
guard let message = messageSnapshot as? [String:String] else { return cell }
let name = message[Constants.MessageFields.name] ?? ""
if let imageURL = message[Constants.MessageFields.imageURL] {
if imageURL.hasPrefix("gs://") {
Storage.storage().reference(forURL: imageURL).getData(maxSize: INT64_MAX) {(data, error) in
if let error = error {
print("Error downloading: \(error)")
return
}
DispatchQueue.main.async {
cell.imageView?.image = UIImage.init(data: data!)
cell.setNeedsLayout()
}
}
} else if let URL = URL(string: imageURL), let data = try? Data(contentsOf: URL) {
cell.imageView?.image = UIImage.init(data: data)
}
cell.textLabel?.text = "sent by: \(name)"
} else {
let text = message[Constants.MessageFields.text] ?? ""
cell.textLabel?.text = name + ": " + text
cell.imageView?.image = UIImage(named: "ic_account_circle")
if let photoURL = message[Constants.MessageFields.photoURL], let URL = URL(string: photoURL),
let data = try? Data(contentsOf: URL) {
cell.imageView?.image = UIImage(data: data)
}
}
return cell
}

While there are two other very good answers, there may be some confusion between a
FIRDocumentSnapshot (Note: renamed to DocumentSnapshot)
Which is returned when you want to get a specific document: someDoc.getDocument(
and
FIRQuerySnapshot (Note: renamed to QuerySnapshot)
Which is returned when an observer is added to a collection or a series of documents is being retrieved: someCollection.getDocuments and then each document within QuerySnapshot is a discreet FIRQueryDocumentSnapshot (renamed to QueryDocumentSnapshot). (e.g. iterate over the QuerySnapshot to get the child QueryDocumentSnapshot)
Note that DocumentSnapshot may return nil in data property if the document doesn't exists, so it can be tested for .exists. Whereas QueryDocumentSnapshot will never be nil (exists is always true) because deleted data is not returned.
In the question, an observer is being added to a collection with
.collection("messages").addSnapshotListener
therefore the data returned is a QuerySnapshot and to store it as a var, the var type would need to match
var messagesQuerySnapshot: QuerySnapshot!
and then inside the listener
db.collection("messages")...addSnapshotListener { querySnapshot, error in
messagesQuerySnapshot = querySnapshot
However, I would not recommend that.
I would suggest a messages class that can be initialize with the data retrieved from Firestore and store those in an array.
class MessagesClass {
var msg_id = ""
var msg = ""
var from = ""
convenience init(withQueryDocSnapshot: QueryDocumentSnapshot) {
//init vars from the document snapshot
}
}
and then a class var datasource array to hold them
var messagesArray = [MessageClass]()
and then code to read the messages, create the message objects and add them to the dataSource array
db.collection("messages")...getDocuments { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
for doc in snapshot.documents {
let aMsg = MessageClass(withQueryDocSnapshot: doc)
self.messagesArray.append(aMsg)
}
}
NOTE: we are not adding an listener here, we are getting the documents one time. If you want to add a listener to watch for users being added, changed or removed, additional code is needed to detect the changes.
See the Firebase Documentation on Viewing Changes Between Snapshots

Replace self.messages.append(documents) with self.messages.append(contentsOf: documents)
The first method takes a single element and the second one takes a collection which is in your case.
https://developer.apple.com/documentation/swift/array/3126937-append
https://developer.apple.com/documentation/swift/array/3126939-append

var messages: [[String: Any]] = []
db.collection("messages").document("hello").collection("newmessages").document("2").collection("hellos").document("K").collection("messages").addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
for doc in documents {
self.messages.append(doc.data())
}
}

Related

URLSession dataTask execution order

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
}
}

Error nil in retrieving image from Firebase Storage

I have trouble retrieving a UIImage from Firebase Storage, the child path seems to be correct, though the image does not get "downloaded" to be displayed. The part about the Firebase Database is working fine, hence retrieving data, whereas the Storage one is now. Code and Firebase path below
I cannot understand whether the problem is in the fact that I nested the function into the .observeSingleEvent of the Database retrieving function or not.
gs://xxxyyy-xxxyyy.appspot.com/images/QhRmIcbF7AOWjZ3nrjFd7TOekrA3/FirstImage.jpg
var cells : [Cella] = []
var imageReference: StorageReference {
return Storage.storage().reference().child("images")
}
var databaseReference: DatabaseReference {
return Database.database().reference()
}
func getDataFromFirebase() -> [Cella]{
let queryRef = databaseReference.queryLimited(toLast: 1)
var appCells : [Cella] = []
queryRef.observeSingleEvent(of: .value, with: { (snapshot) in
for snap in snapshot.children {
var userPhoto : UIImage?
let userSnap = snap as! DataSnapshot
let customerUid = userSnap.key
let userDict = userSnap.value as! [String:AnyObject]
let description = userDict["description"] as! String
let title = userDict["title"] as! String
print(title)
print(String(customerUid))
print(description)
self.descriptionsArray[String(customerUid)] = description
self.titlesArray[String(customerUid)] = title
//error is here BECAUSE it can't retrive the image to be dispalyed. Title and description are fine
self.imageReference.child(String(customerUid)).child("FirstImage.jpg").getData(maxSize: 10*1024*1024, completion: { (data, error) in
if error != nil {
print("\(String(describing: error?.localizedDescription))")
}
else {userPhoto = UIImage(data: data!)}
})
let newCella = Cella(image: userPhoto!, title: title, bodyMessage: description)
appCells.append(newCella)
}
})
return appCells
}
------ UPDATE ------
As suggested I changed to using firebase Firestore and saving there the download URL as well as the other information. Still though, I cannot seem to get the image downloading. New code below.
This is the data retrieved by document.data() :
xxx.yyy#gmail.com => ["userID": QhRmIcbF7AOWjZ3nrjFd7TOekrA3, "userDescription": Route66, "userImageUrl": https://firebasestorage.googleapis.com/v0/b/shardana-61183.appspot.com/o/images%2FQhRmIcbF7AOWjZ3nrjFd7TOekrA3%2FFirstImage.jpg?alt=media&token=dea541bf-d598-414e-b4ed-a917541598d5, "userTitle": Sample]
firestoreUsersDatabase.getDocuments { (querySnapshot, error) in
if let error = error {
print("Error getting the documents: \(error)")
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
let data = document.data()
let imageUrl = data["userImageUrl"] as! String
let title = data["userTitle"] as! String
let description = data["userDescription"] as! String
let urlDownloadReference = self.imageReference.reference(forURL: imageUrl)
urlDownloadReference.getData(maxSize: 10*2014*2014, completion: { (data, error) in
if error != nil {
print("An error occurred: \(String(describing: error?.localizedDescription))")
} else {
guard let imageDownloaded = UIImage(data: data!) else {print("Image url returned nil value ERROR"); return}
let newCell = Cella(image: imageDownloaded, title: title , bodyMessage: description )
print("NEW CELL: Image \(newCell.image)")
appCells.append(newCell)
}
})
}
}
}
yes, I think you're logic needs review. You need to store on your Firestore all the users data, including all the references to needed images. On the other hand, Firebase Storage, which is a different service within Firebase will save the images an will give you download links, but it uses a different logic than Firestore.
See the following example for clarification on what I mean:
https://firebase.google.com/docs/storage/web/download-files

Populating tableview from structs instead of arrays

I'm making a small project to practice serializing JSON in Swift 4 using structs... it gets top stories from the New York Times API and puts them into a table view. Currently I'm getting the data I need from the JSON and filling some arrays with the stuff I need (headlines, abstracts, etc).
Someone advised me to skip that step and instead populate the table view directly from the structs.
struct TopStoriesResponse: Decodable {
let status: String
let results: [Story]
}
struct Story: Decodable {
let title: String
let abstract: String
let url: String
let multimedia: [Multimedia]
private enum CodingKeys: String, CodingKey {
case title
case abstract
case url
case multimedia
}
init(from decoder:Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
abstract = try container.decode(String.self, forKey: .abstract)
url = try container.decode(String.self, forKey: .url)
multimedia = (try? container.decode([Multimedia].self, forKey: .multimedia)) ?? []
}
}
struct Multimedia: Decodable {
let url: String
let type: String
}
var storyData = [Story]()
And in my cellForRowAt method:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "storyCell", for: indexPath) as! StoryTableViewCell
let stories = storyData[indexPath.row]
print("Titles: \(stories.title)")
cell.headlineLabel.text = stories.title
cell.abstractLabel.text = stories.abstract
return cell
}
When I run the app, the table view is empty and my print statement confirmed that stories.title is empty (everything showed up before when I was using arrays).
This is the function that grabs the data if it's applicable here, i'll scrap all the code that passes the data into arrays if I can use the structs instead:
func getJSON(completionHandler: #escaping (Bool) -> ()) {
let jsonUrlString = "https://api.nytimes.com/svc/topstories/v1/business.json?api-key=f4bf2ee721031a344b84b0449cfdb589:1:73741808"
guard let url = URL(string: jsonUrlString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data, err == nil else {
print(err!)
return
}
do {
let response = try
JSONDecoder().decode(TopStoriesResponse.self, from: data)
// Pass results into arrays (title, abstract, url, image)
for result in response.results {
let headlines = result.title
let abstracts = result.abstract
let url = result.url
self.headlines.append(headlines)
self.abstracts.append(abstracts)
self.urls.append(url)
for imageResults in result.multimedia {
let images = imageResults.url
self.images.append(images)
}
}
completionHandler(true)
} catch let jsonErr {
print("Error serializing JSON", jsonErr)
}
}.resume()
}
Do I need to pass the data back into the structs the way I was doing it with the arrays? I was under the impression that let response = try JSONDecoder().decode(TopStoriesResponse.self, from: data) did that already.
You need
let response = try JSONDecoder().decode(TopStoriesResponse.self, from: data)
self.storyData = response.results
DispatchQueue.main.async {
self.tableView.reloadData()
}
the other arrays content is irrelevant here as you don't use them as the dataSource of the table
There seems to be a mismatch in your data models. On the one hand, your JSON data is being put into four arrays: self.headlines, self.abstracts, self.urls, and self.images. But your table view knows nothing about any of that; it depends entirely on a different array, storyData. You need to bring those two data models together, if you see what I mean. Download the data, rejigger it into storyData, and then tell the table view to reloadData.

Save json to CoreData as String and use the String to create array of objects

I am creating an app for a radio station and I want to store "show" objects into an array. I use a webserver to supply json data to populate the array, but I want to store this json data into CoreData as a string so that access to the array doesn't depend on internet connection. Therefore, I want to update the string in CoreData on app launch, but create an array based off of the string stored in CoreData not on the json data from the webserver.
Here's my function to download the json data from the webserver and store it into a string:
func downloadShows() {
let urlPath = "http://dogradioappdatabase.com/shows.php"
guard let url = URL(string: urlPath) else {return}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let dataResponse = data,
error == nil else {
print(error?.localizedDescription ?? "Response Error")
return }
let jsonAsString = self.jsonToString(json: dataResponse)
DispatchQueue.main.async {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let task2 = WebServer(context: context) // Link Task & Context
task2.showsArray = jsonAsString
print(jsonAsString)
(UIApplication.shared.delegate as! AppDelegate).saveContext()
}
}
task.resume()
}
func jsonToString(json: Data) -> String {
let convertedString: String
convertedString = String(data: json, encoding: String.Encoding.utf8)! // the data will be converted to the string
return convertedString
}
Here's the function to create the shows array from the fetched json from CoreData:
func createShowsArray () -> Array<ShowModel> {
var array: Array<ShowModel> = Array()
var tasks: [WebServer] = []
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do {
tasks = try context.fetch(WebServer.fetchRequest())
}
catch {
print("Fetching Failed")
}
let arrayAsString: String = tasks[0].showsArray!
print(arrayAsString)
do {
let data1 = arrayAsString.data(using: .utf8)!
let decoder = JSONDecoder()
array = try decoder.decode([ShowModel].self, from:
data1)
} catch let parsingError {
print("Error", parsingError)
}
return array
}
However, this does not correctly load the data into an array. I printed the value I saved to CoreData in the downloadShows() function (jsonAsString) and got this as a response:
[{"Name":"Example Show 2","ID":"2","Description":"This ...
But when I fetched the string from CoreData in the createShowsArray() function (arrayAsString), it had added "DOG_Radio.ShowModel"
[DOG_Radio.ShowModel(Name: "Example Show 2", ID: "2", Description: "This ...
The JSON Decoder does not decode arrayAsString into an actual array. It throws this back:
Error dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "Invalid value around character 1." UserInfo={NSDebugDescription=Invalid value around character 1.})))
Sorry for the long question, I just don't know how to use CoreData to save json as String then convert that String into an array later
It's a bad practice to store json data or 'whole raw data' into CoreData. Instead Store the Show itself as a NSManagedObject.
You can do this by converting the JSON data to an Object (which it looks like you are already doing), then creating CoreData NSManagedObjects from them.
Realistically if you have no trouble converting the data from JSON there is no need to convert it to a string before saving to CoreData. You can simply store the Data as NSData, i.e. transformable or binary data and reconvert it later if your fetch to the server fails.
However, thats not that reliable and much harder to work with in the long run. The data could be corrupt and/or malformed.
In short, you need a Data Model and a JSON readable Data Structure you can Convert to your Data Model to for CoreData to manage. This will become important later when you want to allow the user to update, remove, save or filter individual Show's.
Codable will allow you to covert from JSON to a Struct with JSONDecoder().decode(_:from:).
ShowModelCodeable.swift
import Foundation
struct ShowModelCodeable: Codable {
var name: String?
var description: String?
var producer: String?
var thumb: String?
var live: String?
var banner: String?
var id: String?
enum CodingKeys: String, CodingKey {
case name = "Name"
case id = "ID"
case description = "Description"
case producer = "Producer"
case thumb = "Thumb"
case live = "Live"
case banner = "Banner"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
description = try values.decode(String.self, forKey: .description)
producer = try values.decode(String.self, forKey: .producer)
thumb = try values.decode(String.self, forKey: .thumb)
live = try values.decode(String.self, forKey: .live)
banner = try values.decode(String.self, forKey: .banner)
id = try values.decode(String.self, forKey: .id)
}
func encode(to encoder: Encoder) throws {
}
}
Next, We'll need a Core Data Stack and a CoreData Entity. Its very common to create a Core Data Stack as a Class Singleton that can be accessed anywhere in your app. I've included one with basic operations:
DatabaseController.Swift
import Foundation
import CoreData
class DatabaseController {
private init() {}
//Returns the current Persistent Container for CoreData
class func getContext () -> NSManagedObjectContext {
return DatabaseController.persistentContainer.viewContext
}
static var persistentContainer: NSPersistentContainer = {
//The container that holds both data model entities
let container = NSPersistentContainer(name: "StackOverflow")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
//TODO: - Add Error Handling for Core Data
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
class func saveContext() {
let context = self.getContext()
if context.hasChanges {
do {
try context.save()
print("Data Saved to Context")
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate.
//You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
/* Support for GRUD Operations */
// GET / Fetch / Requests
class func getAllShows() -> Array<ShowModel> {
let all = NSFetchRequest<ShowModel>(entityName: "ShowModel")
var allShows = [ShowModel]()
do {
let fetched = try DatabaseController.getContext().fetch(all)
allShows = fetched
} catch {
let nserror = error as NSError
//TODO: Handle Error
print(nserror.description)
}
return allShows
}
// Get Show by uuid
class func getShowWith(uuid: String) -> ShowModel? {
let requested = NSFetchRequest<ShowModel>(entityName: "ShowModel")
requested.predicate = NSPredicate(format: "uuid == %#", uuid)
do {
let fetched = try DatabaseController.getContext().fetch(requested)
//fetched is an array we need to convert it to a single object
if (fetched.count > 1) {
//TODO: handle duplicate records
} else {
return fetched.first //only use the first object..
}
} catch {
let nserror = error as NSError
//TODO: Handle error
print(nserror.description)
}
return nil
}
// REMOVE / Delete
class func deleteShow(with uuid: String) -> Bool {
let success: Bool = true
let requested = NSFetchRequest<ShowModel>(entityName: "ShowModel")
requested.predicate = NSPredicate(format: "uuid == %#", uuid)
do {
let fetched = try DatabaseController.getContext().fetch(requested)
for show in fetched {
DatabaseController.getContext().delete(show)
}
return success
} catch {
let nserror = error as NSError
//TODO: Handle Error
print(nserror.description)
}
return !success
}
}
// Delete ALL SHOWS From CoreData
class func deleteAllShows() {
do {
let deleteFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "ShowModel")
let deleteALL = NSBatchDeleteRequest(fetchRequest: deleteFetch)
try DatabaseController.getContext().execute(deleteALL)
DatabaseController.saveContext()
} catch {
print ("There is an error in deleting records")
}
}
Finally, we need a way to get the JSON data and convert it to our Objects, then Display it. Note that when the update button is pressed, it fires getDataFromServer(). The most important line here is
self.newShows = try JSONDecoder().decode([ShowModelCodeable].self, from: dataResponse)
The Shows are being pulled down from your Server, and converted to ShowModelCodeable Objects. Once newShows is set it will run the code in didSet, here you can delete all the Objects in the context, then run addNewShowsToCoreData(_:) to create new NSManagedObjects to be saved in the context.
I've created a basic view controller and programmatically added a tableView to manage the data. Here, Shows is your NSManagedObject array from CoreData, and newShows are new objects encoded from json that we got from the server request.
ViewController.swift
import Foundation
import UIKit
import CoreData
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
// Properties
var Shows:[ShowModel]?
var newShows:[ShowModelCodeable]? {
didSet {
// Remove all Previous Records
DatabaseController.deleteAllShows()
// Add the new spots to Core Data Context
self.addNewShowsToCoreData(self.newShows!)
// Save them to Core Data
DatabaseController.saveContext()
// Reload the tableView
self.reloadTableView()
}
}
// Views
var tableView: UITableView = {
let v = UITableView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
lazy var updateButton: UIButton = {
let b = UIButton()
b.translatesAutoresizingMaskIntoConstraints = false
b.setTitle("Update", for: .normal)
b.setTitleColor(.black, for: .normal)
b.isEnabled = true
b.addTarget(self, action: #selector(getDataFromServer), for: .touchUpInside)
return b
}()
override func viewWillAppear(_ animated: Bool) {
self.Shows = DatabaseController.getAllShows()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.register(ShowCell.self, forCellReuseIdentifier: ShowCell.identifier)
self.layoutSubViews()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//TableView -
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return DatabaseController.getAllShows().count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// 100
return ShowCell.height()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: ShowCell.identifier) as! ShowCell
self.Shows = DatabaseController.getAllShows()
if Shows?.count != 0 {
if let name = Shows?[indexPath.row].name {
cell.nameLabel.text = name
}
if let descriptionInfo = Shows?[indexPath.row].info {
cell.descriptionLabel.text = descriptionInfo
}
} else {
print("No shows bros")
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Show the contents
print(Shows?[indexPath.row] ?? "No Data For this Row.")
}
func reloadTableView() {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
func layoutSubViews() {
let guide = self.view.safeAreaLayoutGuide
let spacing: CGFloat = 8
self.view.addSubview(tableView)
self.view.addSubview(updateButton)
updateButton.topAnchor.constraint(equalTo: guide.topAnchor, constant: spacing).isActive = true
updateButton.leftAnchor.constraint(equalTo: guide.leftAnchor, constant: spacing * 4).isActive = true
updateButton.rightAnchor.constraint(equalTo: guide.rightAnchor, constant: spacing * -4).isActive = true
updateButton.heightAnchor.constraint(equalToConstant: 55.0).isActive = true
tableView.topAnchor.constraint(equalTo: updateButton.bottomAnchor, constant: spacing).isActive = true
tableView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: guide.bottomAnchor, constant: spacing).isActive = true
}
#objc func getDataFromServer() {
print("Updating...")
let urlPath = "http://dogradioappdatabase.com/shows.php"
guard let url = URL(string: urlPath) else {return}
let task = URLSession.shared.dataTask(with: url) {
(data, response, error) in
guard let dataResponse = data, error == nil else {
print(error?.localizedDescription ?? "Response Error")
return }
do {
self.newShows = try JSONDecoder().decode([ShowModelCodeable].self, from: dataResponse)
} catch {
print(error)
}
}
task.resume()
}
func addNewShowsToCoreData(_ shows: [ShowModelCodeable]) {
for show in shows {
let entity = NSEntityDescription.entity(forEntityName: "ShowModel", in: DatabaseController.getContext())
let newShow = NSManagedObject(entity: entity!, insertInto: DatabaseController.getContext())
// Create a unique ID for the Show.
let uuid = UUID()
// Set the data to the entity
newShow.setValue(show.name, forKey: "name")
newShow.setValue(show.description, forKey: "info")
newShow.setValue(show.producer, forKey: "producer")
newShow.setValue(show.thumb, forKey: "thumb")
newShow.setValue(show.live, forKey: "live")
newShow.setValue(show.banner, forKey: "banner")
newShow.setValue(show.id, forKey: "id")
newShow.setValue(uuid.uuidString, forKey: "uuid")
}
}
}
Try this it's work for me
// Convert JSON to String
func jsonToString(json: AnyObject)->String{
do {
let data1 = try JSONSerialization.data(withJSONObject: json, options: JSONSerialization.WritingOptions.prettyPrinted) // first of all convert json to the data
let convertedString = String(data: data1, encoding: String.Encoding.utf8) // the data will be converted to the string
return convertedString! // <-- here is ur string
} catch let myJSONError {
print(myJSONError)
}
return ""
}
// Convert JSON String to Dict
func convertToDictionary(text: String) -> NSDictionary!{
if let data = text.data(using: .utf8) {
do {
return try JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary
} catch {
print(error.localizedDescription)
}
}
return nil
}

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.

Resources