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")
}
Related
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.
"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 trying to store a dictionary in UserDefaults and always get app crash when the code runs. Here is the sample code which crashes the app when it is executed. I tried to cast it as NSDictionary or make it NSDictionary initially - got the same result.
class CourseVC: UIViewController {
let test = [1:"me"]
override func viewDidLoad() {
super.viewDidLoad()
defaults.set(test, forKey: "dict1")
}
}
Dictionaries are Codable objects by default, you can use the following extensions to save them to UserDefaults
extension UserDefaults {
func object<T: Codable>(_ type: T.Type, with key: String, usingDecoder decoder: JSONDecoder = JSONDecoder()) -> T? {
guard let data = self.value(forKey: key) as? Data else { return nil }
return try? decoder.decode(type.self, from: data)
}
func set<T: Codable>(object: T, forKey key: String, usingEncoder encoder: JSONEncoder = JSONEncoder()) {
let data = try? encoder.encode(object)
self.set(data, forKey: key)
}
}
They can be used like this:
let test = [1:"me"]
UserDefaults.standard.set(object: test, forKey: "test")
let testFromDefaults = UserDefaults.standard.object([Int: String].self, with: "test")
This extension and many others are part of SwifterSwift, you might want to use it for your next iOS project :)
To store a NSDictionary (with non-string key) in NSUserDefaults you need to convert them to NSData first.
Try this
let test = [1:"me"]
override func viewDidLoad() {
super.viewDidLoad()
let data = NSKeyedArchiver.archivedData(withRootObject: test)
let defaults = UserDefaults.standard
defaults.set(data, forKey: "dict1")
if let data2 = defaults.object(forKey: "dict1") as? NSData {
let dict = NSKeyedUnarchiver.unarchiveObject(with: data2 as Data)
print(dict)
}
}
I try to implement a simple shopping list swift application for iOS as a personal project. I did follow a guide for iOS on youtube.
My question is how do I parse the Item object from firebase to my ShoppingListItem swift object? If I execute the following code, it doesn't show any error message but it does not show any results either. If I uncomment all "items" lines, it shows the expected results without the item information.
Here is a screenshot from the firebase console of my firebase firestore structure / example object
Thanks in advance!
ShoppingListItem.swift
import Foundation
import FirebaseFirestore
protocol DocumentSerializable {
init?(dictionary: [String: Any])
}
struct ShoppingListItem {
var shoppingItemID: String
var priority: Int
var quantity: Int
var item: Item
var dictionary: [String: Any] {
return [
"shoppingItemID": shoppingItemID,
"priority": priority,
"quantity": quantity,
"item": item,
]
}
}
extension ShoppingListItem: DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let shoppingItemID = dictionary["shoppingItemID"] as? String,
let priority = dictionary["priority"] as? Int,
let quantity = dictionary["quantity"] as? Int,
let item = dictionary["item"] as? Item
else { return nil }
self.init(shoppingItemID: shoppingItemID, priority: priority, quantity: quantity, item: item)
}
}
struct Item {
var itemID: String
var lastPurchase: String
var name: String
var note: String
var picturePath: String
var dictionary: [String: Any] {
return [
"itemID": itemID,
"lastPurchase": lastPurchase,
"name": name,
"note": note,
"picturePath": picturePath,
]
}
}
extension Item: DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let itemID = dictionary["itemID"] as? String,
let lastPurchase = dictionary["lastPurchase"] as? String,
let name = dictionary["name"] as? String,
let note = dictionary["note"] as? String,
let picturePath = dictionary["picturePath"] as? String else { return nil }
self.init(itemID: itemID, lastPurchase: lastPurchase, name: name, note: note, picturePath: picturePath)
}
}
Get Data call in TableViewController.swift
db.collection("shoppingList").getDocuments(){
querySnapshot, error in
if let error = error {
print("error loading documents \(error.localizedDescription)")
} else{
self.shoppingArray = querySnapshot!.documents.flatMap({ShoppingListItem(dictionary: $0.data())})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
I used the Codable protocol.
I used this as an extension to the Encodable Protocol:
extension Encodable {
/// Returns a JSON dictionary, with choice of minimal information
func getDictionary() -> [String: Any]? {
let encoder = JSONEncoder()
guard let data = try? encoder.encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any]
}
}
}
Then I use this to decode:
extension Decodable {
/// Initialize from JSON Dictionary. Return nil on failure
init?(dictionary value: [String:Any]){
guard JSONSerialization.isValidJSONObject(value) else { return nil }
guard let jsonData = try? JSONSerialization.data(withJSONObject: value, options: []) else { return nil }
guard let newValue = try? JSONDecoder().decode(Self.self, from: jsonData) else { return nil }
self = newValue
}
}
Make your two structs conform to Codable (Item first, then ShoppingListItem). Of course, this may not work for the existing data stored in Firestore. I would first put data into Firestore via the getDictionary() (in a new collection), then try to read it back into your tableView.
You may also want to print the actual error when trying to Decode your data, this will greatly help you pinpoint the data error if there's any.
extension Decodable {
/// Initialize from JSON Dictionary. Return nil on failure
init?(dictionary value: [String:Any]) {
guard JSONSerialization.isValidJSONObject(value) else {
return nil
}
do {
let jsonData = try JSONSerialization.data(withJSONObject: value, options: [])
let newValue = try JSONDecoder().decode(Self.self, from: jsonData)
self = newValue
}
catch {
log.error("failed to serialize data: \(error)")
return nil
}
}
}
It's now easier with Swift 4 to encode / decode to and from JSON or Properties list.
But I can't find how to encode to Data using Codable, without using Objective-C methods initWithCoder and encodeWithCoder.
Considering this simple class:
struct Question: Codable {
var title: String
var answer: Int
var question: Int
}
How can I encode it to Data using CodingKeys and not initWithCoder and encodeWithCoder?
EDIT:
I also need to be able to deserialize objects previously saved in userdefaults using NSKeyedArchiver.
Well, you no longer need NSKeyedArchiver.
Try this:
let questionObj = Question(title: "WWDC, 2017", answer: 1,question:1)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(questionObj) {
UserDefaults.standard.set(encoded, forKey: "K_Question")
}
let decoder = JSONDecoder()
if let questionData = UserDefaults.standard.data(forKey: "K_Question"),
let question = try? decoder.decode(Question.self, from: questionData) {
print(question.title)
print(question.answer)
print(question.question)
}
Swift 5: a great simple extension for UserDefaults:
extension UserDefaults {
func save<T: Codable>(_ object: T, forKey key: String) {
let encoder = JSONEncoder()
if let encodedObject = try? encoder.encode(object) {
UserDefaults.standard.set(encodedObject, forKey: key)
UserDefaults.standard.synchronize()
}
}
func getObject<T: Codable>(forKey key: String) -> T? {
if let object = UserDefaults.standard.object(forKey: key) as? Data {
let decoder = JSONDecoder()
if let decodedObject = try? decoder.decode(T.self, from: object) {
return decodedObject
}
}
return nil
}
}
Usage
save
UserDefaults.standard.save(currentUser, forKey: "currentUser")
get
let user: User? = UserDefaults.standard.getObject(forKey: "currentUser")
Well it can be achieved via JSONEncoder and JSONDecoder.
struct Question: Codable {
var title: String
var answer: Int
var question: Int
}
let questionObj = Question(title: "Swift", answer: "Open Source",question:1)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(questionObj) {
if let json = String(data: encoded, encoding: .utf8) {
print(json)
}
let decoder = JSONDecoder()
if let decoded = try? decoder.decode(Question.self, from: encoded) {
print(decoded)
}
}
struct Question: Codable {
var title: String
var answer: Int
var question: Int
}
class UserDefaults_Question {
static let key = "myapp.trick.question"
static var value: UserDefaults_Question? {
get {
guard let data = UserDefaults.standard.data(forKey: key) else {
print("no model for key: \(key)")
return nil
}
guard let model = try? JSONDecoder().decode(UserDefaults_Question.self, from: data) else {
print("failed to decode model for key: \(key)")
return nil
}
print("did load model for key: \(key)")
return model
}
set {
guard let value = newValue, let data: Data = try? JSONEncoder().encode(value) else {
print("removing model for key: \(key)")
UserDefaults.standard.removeObject(forKey: key)
return
}
print("inserting model for key: \(key)")
UserDefaults.standard.set(data, forKey: key)
}
}
}
UserDefaults_Question.value = Question(title: "Next President", answer: 666, question: -1)