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 thi
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
}
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")
}
}
}