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.
Related
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())
}
}
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
}
"collection_listings" = (
{
"body_html" = "";
"collection_id" = 57229082710;
"default_product_image" = "<null>";
handle = men;
image = {
"created_at" = "2018-05-02T01:34:16-04:00";
src = "https://cdn.shopify.com/s/files/1/2331/3377/collections/men.jpg?v=1525239256";
};
"published_at" = "2018-05-02T01:34:16-04:00";
"sort_order" = manual;
title = Men;
"updated_at" = "2018-05-02T08:01:58-04:00";
}
How to print this data in the simulator using swift 4?
While I'm trying to print this data, I get this error:
typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath:
[], debugDescription: "Expected to decode Array but found a
dictionary instead.", underlyingError: nil))
Here is my updated code:
import UIKit
struct product: Decodable
{
let product_id : String
let title : String
let image : String
}
class ViewController: UIViewController,UICollectionViewDataSource
{
var products = product
#IBOutlet weak var productCell: UICollectionView!
override func viewDidLoad()
{
super.viewDidLoad()
productCell.dataSource = self
guard let url = URL(string: "https://psofttech-test.myshopify.com/admin/collection_listings.json") else { return }
var request = URLRequest(url: url)
request.httpMethod = "GET"
let session = URLSession.shared
session.dataTask(with: url) { (data, response, error) in
if error == nil
{
do
{
let json = try JSONSerialization.jsonObject(with: data!) as? [String: Any]
self.products = try JSONDecoder().decode([product].self, from: data!)
print(self.products, "dddd")
for info in self.products
{
self.productCell.reloadData()
}
}
catch
{
print(error)
}
}
}.resume()
print(products.self,"0000")
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return products.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "productCollection", for: indexPath) as! productCollectionViewCell
cell.proLBL.text = products[indexPath.row].title
return cell
}
}
See if this code helps you..
if let data = data as? [String: Any] {
if let data = data["collection_listings"] as? NSArray {
for data in data {
if let data = data as [String: Any] {
}
}
}
}
Now you can use codable to parse json data in Swift 4.0.
Using Codable, we can model JSONObject or PropertyList file into
equivalent Struct or Classes by writing very few lines of code. We
don’t have to write the constructor for the properties in the
objects. It’s all handed by Codable. We just need to extend our model
to conform to the Codable, Decodable or Encodable protocol.
Mismatch between the strong data types of Swift and lose data types
of JSON has been internally handled by Swift compiler. We can now
handle Swift Data types like Date, URL, Float etc
Complex JSON can be modelled easily using Nesting Structs for
readability.
Parsing actual JSON become one-liner using JSONDecoder
You can refer to this app for using the codable : Demo App for Codable Swift
You're using the wrong object to decode. Basically it's telling you that the object type you sent to the decoder does not match the JSON. In your case (without seeing your code it's hard to tell exactly) it seems the object you provided is of type of array, and the JSON is a dictionary. This is the method I typically use:
Specify a struct of the type you want to decode:
struct Response {
var collection_listings: Listing
}
struct Listing {
var collection_id: String
}
In your decoder specific something like:
let decoder = JSONDecoder()
let apiResponse = try decoder.decode(Response.self, from: data)
I'm struggling to get my CoreData objects into JSON so that I can use it to send to a web server.
This is how I currently fetch my objects from CoreData:
func fetchRecord() -> [Record] {
do {
records = try context.fetch(Record.fetchRequest())
} catch {
print("Error fetching data from CoreData")
}
return records
}
I am able to display this on to my tableView this way:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "recordCell", for: indexPath) as! RecordCell
cell.nameLbl.text = records[indexPath.row].name
cell.quantityLbl.text = "Quantity: \(String(records[indexPath.row].quantity))"
cell.dateLbl.text = dateString(date: records[indexPath.row].date)
return cell
}
I have attempted to loop inside my request like this:
for rec in records {
print(rec)
}
that gives out this:
I have read a lot about ways to achieve this but none of them seem to really be of beneficial to me. Most of the examples out there shows how to get JSON to CoreData and not the other way. Does anyone know any good tutorials or documentation that can help me achieve this?
In Swift 4+ you can take advantage of the Encodable protocol and add the functionality directly to your Core Data object.
Assuming your NSManagedObject subclass extension looks like
extension Record {
#NSManaged public var date: Date
#NSManaged public var name: String
#NSManaged public var quantity: Int32
#NSManaged public var synched: Bool
#NSManaged public var uuid: String
...
Adopt Encodable
extension Record : Encodable {
and add
private enum CodingKeys: String, CodingKey { case date, name, quantity, synched, uuid }
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(date, forKey: .date)
try container.encode(name, forKey: .name)
try container.encode(quantity, forKey: .quantity)
try container.encode(synched, forKey: .synched)
try container.encode(uuid, forKey: .uuid)
}
Then you can easily encode the records to JSON
do {
records = try context.fetch(Record.fetchRequest())
let jsonData = try JSONEncoder().encode(records)
} catch {
print("Error fetching data from CoreData", error)
}
Here the code as an extension.
Based on KavyaKavita's answer.
extension NSManagedObject {
func toJSON() -> String? {
let keys = Array(self.entity.attributesByName.keys)
let dict = self.dictionaryWithValues(forKeys: keys)
do {
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
let reqJSONStr = String(data: jsonData, encoding: .utf8)
return reqJSONStr
}
catch{}
return nil
}
}
Usage:
let jsonString = YourCoreDataObject.toJSON()
print(jsonString)
You can convert your NSManageObject subclass object into dictionary by using following code
let record = recArray[index]
let keys = Array(record.entity.attributesByName.keys)
let dict = record.dictionaryWithValues(forKeys: keys)
After that you can use jsonserialization to convert that dictionary into json object
do{
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
let reqJSONStr = String(data: jsonData, encoding: .utf8)
print(reqJSONStr!)
}catch{
}
Hope this will help.
If you want you can get the results in dictionary format from core data using below :
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName:"Record")
fetchRequest.resultType = .dictionaryResultType
do {
records = try context.fetch(fetchRequest)
} catch {
print("Error fetching data from CoreData")
}
I'm struggling with multithreading in news app. The thing is - my application freezes often when I scroll table view after data was parsed and loaded and its way too often. I think I'm some kind of wrong of reloading data every time.
First part:
final let urlString = "http://api.to.parse"
Here I create array of structs to fill in my data
struct jsonObjects {
var id : Int
var date : String
var title : String
var imageURL : URL
}
var jsonData = [jsonObjects]()
Here's my viewDidLoad of tableView
override func viewDidLoad() {
super.viewDidLoad()
// MARK : - Download JSON info on start
JsonManager.downloadJsonWithURL(urlString: urlString, сompletion: {(jsonArray) -> Void in
guard let data = jsonArray else { print("Empty dude"); return;}
for jsonObject in data {
if let objectsDict = jsonObject as? NSDictionary {
guard
let id = objectsDict.value(forKey: "id") as? Int,
let date = objectsDict.value(forKey: "date") as? String,
let titleUnparsed = objectsDict.value(forKey: "title") as? NSDictionary,
let title = (titleUnparsed as NSDictionary).value(forKey: "rendered") as? String,
let imageString = objectsDict.value(forKey: "featured_image_url") as? String,
let imageURL = NSURL(string: imageString) as URL?
else {
print("Error connecting to server")
return
}
There I go with appending filled structure to array:
self.jsonData.append(jsonObjects(id: id, date: date, title: title,
imageURL: imageURL))
}
}
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
})
})
and downloadJsonWithURL is simply:
class JsonManager {
class func downloadJsonWithURL(urlString: String, сompletion: #escaping (NSArray?) -> Void) {
guard let url = NSURL(string: urlString) else { print("There is no connection to the internet"); return;}
URLSession.shared.dataTask(with: url as URL, completionHandler: { (data, response, error) -> Void in
guard let parseData = data else { print("There is no data"); return;}
if let jsonObj = try? JSONSerialization.jsonObject(with: parseData, options: .allowFragments)
as? NSArray {
сompletion(jsonObj)
}
}).resume()
}
And finally - I input that in my TableViewCell:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return jsonData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "newscell") as? NewsTableViewCell else {
fatalError("Could not find cell by identifier")
}
guard let imageData = NSData(contentsOf: jsonData[indexPath.row].imageURL) else {
fatalError("Could not find image")
}
cell.newsTitleLabel.text = self.jsonData[indexPath.row].title
cell.newsTitleLabel.font = UIFont.boldSystemFont(ofSize: 20.0)
cell.newsImageView.image = UIImage(data: imageData as Data)
return cell
}
So there are two questions: how should I distribute my threads and how should I call them so that I have smooth and nice tableview with all downloaded data? and how should I reload data in cell?
Your issue is caused by the imageData its blocking the main thread. The best way to solve this is to download all the images into an image cache. And I would most certainly remove the downloading of images from within the cellForRowAtIndexPath.
Downloading data, parsing in background thread, the updating the UI on main-thread.
Basically if you do correctly like this, everything will be okay.
So you may need to double check one more time if you are rendering UI on main-thread.
On the debugging panel, there's pause/play button.
So whenever your app frozen, try to pause the app immediately:
1) Then check if any of your UI method is running on background-thread.
2) Check if your downloading task or parsing json doing on main-thread.
If it falls under above cases, it needs to be correct.