Now I do my RSSReader app and need to add CoreData on it.
I use pod FeedKit, that have class RSSFeed which receive data from chanel. That data I need to save to CoreData and then display on my app. I have DataManager that has method saveChanel from CoreData.
My data I get on DataManager. DataManager have method loadChanel that have method saveChanel from CoreData (PersistanceManager.shared.saveChanell(feed: RSSFeed)).
My coreDataModel has 2 entity "Item" and "Chanel".
And I know that I have problems in my code on PersistanceManager(code below). Can somebody help with it?
My DataManager is:
class DataManager {
static let sharedInstance = DataManager()
// var dataManagerDelegate: DataManagerDelegate?
var myLinkString: String!
private init() {}
// MARK: - Public
func loadChanel(completion: #escaping ([Chanel]?, Error?, Bool) -> Void, channelAddres: String) {
guard let myLink = myLinkString, let linkUrl = URL(string: myLink) else { return }
let parser = FeedParser(URL: linkUrl)
let result = parser.parse()
guard let feed = result.rssFeed, result.isSuccess else { return }
PersistanceManager.shared.saveChanell(feed: RSSFeed)
}
func loadChannels(completion: #escaping ([Chanel]?, Error?, Bool) -> Void) {
func performCompletion(channels: [Chanel]?, error: Error?, finished: Bool) {
DispatchQueue.main.async {
completion(channels, error, finished)
}
}
/*
* Fetch local channels
*/
let cachedChannels = PersistanceManager.shared.fetchRssChannels()
performCompletion(channels: cachedChannels, error: nil, finished: false)
/*
* Load channels from server
*/
myLinkString = "https://..."
guard let linkUrl = URL(string: myLinkString) else { return }
let parser = FeedParser(URL: linkUrl)
let result = parser.parse()
guard let feed = result.rssFeed, result.isSuccess else { return }
/*
* Store data to database
*/
PersistanceManager.shared.saveChanell(channel: Chanel, feed: RSSFeed)
/*
* Get actual posts from data base and return
*/
let updatedChannels = PersistanceManager.shared.fetchRssChannels()
performCompletion(channels: updatedChannels, error: nil, finished: true)
}
func saveContext() {
PersistanceManager.shared.saveContext()
}
}
On DataManager I have 3 errors on the next lines:
PersistanceManager.shared.saveChanell(feed: RSSFeed)
PersistanceManager.shared.saveChanell(channel: Chanel, feed: RSSFeed)
Errors are:
"Cannot convert value of type 'RSSFeed.Type' to expected argument type 'RSSFeed'");
"Cannot convert value of type 'Chanel.Type' to expected argument type 'Chanel'"
And PersistanceManager:
import Foundation
import CoreData
import FeedKit
class PersistanceManager {
// wrapper for core data
static var shared = PersistanceManager()
private init() {}
func saveChanell(feed: RSSFeed) {
let channel = createNewChanel(with: feed.title)
channel?.rssDescription = feed.description
channel?.pubdate = feed.pubDate! as NSDate
channel?.link = feed.link
channel?.language = feed.language
channel?.creator = feed.dublinCore?.dcCreator
guard let feedItems = feed.items else {
return
}
guard let _ = channel?.item else {
channel?.item = NSSet()
}
guard let channelItems = channel?.item else {
return
}
for feedItem in feedItems {
}
// 5
do {
try context.save()
print("Channel saved")
} catch {}
}
func saveChanell(channel: Chanel, feed: RSSFeed) {
channel.title = feed.title
channel.rssDescription = feed.description
channel.pubdate = feed.pubDate! as NSDate
//channel.pubDate = NSDate(timeIntervalSince1970: feed.pubDate?.timeIntervalSince1970)
channel.link = feed.link
channel.language = feed.language
//channel.isLastUsed
channel.creator = feed.dublinCore?.dcCreator
guard let items = feed.items else {
return
}
// 3
for item in items {
guard let mediaLink = item.media?.mediaThumbnails?.first?.attributes?.url else {
continue
}
let rssItem = createRssItem(with: mediaLink, in: context)
rssItem?.title = item.title
// rssItem?.pubdate = (item.pubDate as! NSDate)
rssItem?.link = item.link
rssItem?.itemDescription = item.description
rssItem?.category = item.categories?.first?.value
channel.item?.adding(rssItem as Any)
}
// 5
do {
try context.save()
print("Channel saved")
} catch {}
}
// 4
private func createRssItem(with mediaLink: String, in context: NSManagedObjectContext) -> Item? {
let newRssItem = NSEntityDescription.insertNewObject(forEntityName: "Item", into: context) as? Item
newRssItem?.mediaLink = mediaLink
print("RSS Item created")
return newRssItem
}
func fetchRssChannels() -> [Chanel]? {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Chanel")
do {
let channels = try self.context.fetch(fetchRequest) as? [Chanel]
return channels
} catch let error {
print(error)
}
print("RSS channels fetched")
return nil
}
// 2 - а тот ли тут канал
func createNewChanel(with chanel: Chanel) -> Chanel? {
if let findRssChannelCD = findRssChannel(title: chanel.title) {
print("findRssChannelCD")
return findRssChannelCD
}
let newRssChannelCD = NSEntityDescription.insertNewObject(forEntityName: "Chanel", into: context) as? Chanel
newRssChannelCD?.title = chanel.title
newRssChannelCD?.item = NSSet()
do {
try context.save()
print("newChanelSaved")
} catch {}
return newRssChannelCD
}
// 1
private func findRssChannel(title: String?) -> Chanel? {
guard let title = title else {
return nil
}
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Chanel")
request.predicate = NSPredicate(format: "title = %#", title)
do {
let users = try context.fetch(request) as? [Chanel]
return users?.first
} catch {}
return nil
}
// MARK: - Core Data stack
var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "NewsForIphone")
container.loadPersistentStores(completionHandler: { (_, 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.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
public func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} 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)")
}
}
}
}
And I know that I have problems in my code on PersistanceManager(code below), and errors in DataManager. Can somebody help with it?
The issue is caused by this line:
PersistanceManager.shared.saveChanell(feed: RSSFeed)
You are specifying actual class name instead of the object, you need to write:
PersistanceManager.shared.saveChanell(feed: feed)
Related
I am writing an iOS App in Swift.
In my Home page (HomeLandingViewController.swift), I have to call two APIs parallel which gives me a list of images, and I have to download all those images, and then dump in CoreData. Till this process completes, I have to show some loading animation etc in UI.
FLOW:
Homepage VC loads > Start Animation > Call API 1 and Call API 2 parallel > Receive image arrays from API 1 and API 2 > get DATA of all those images > Dump into Coredata > Notify homepage VC that work is done > Stop Animation
For this purpose, I have made a dedicated class (IconsHelper.swift)
I am using Moya networking library.
The issue is that things are not working as expected. Because things are working asynchronous, the homepage VC is getting notified even before images are downloaded.
My Code Snippets:
IconsHelper.shared.getNewIconsFromServer()
class IconsHelper {
static let shared: IconsHelper = .init()
var group:DispatchGroup?
//Get Icons from API 1 and API 2:
func getNewIconsFromServer() {
group = DispatchGroup()
group?.enter()
let dispatchQueue_amc = DispatchQueue(label: "BackgroundIconsFetch_AMC", qos: .background)
dispatchQueue_amc.async(group: group) {
self.getNewIcons(type: .amcIcons)
}
if group?.hasGroupValue() ?? false {
group?.leave()
Log.b("CMSIcons: Group Leave 1")
}
group?.enter()
let dispatchQueue_bank = DispatchQueue(label: "BackgroundIconsFetch_Bank", qos: .background)
dispatchQueue_bank.async(group: group) {
self.getNewIcons(type: .bankIcons)
}
if group?.hasGroupValue() ?? false {
group?.leave()
Log.b("CMSIcons: Group Leave 2")
}
group?.notify(queue: .global(), execute: {
Log.b("CMSIcons: All icons fetched from server.")
})
}
func getNewIcons(type: CMSIconsTypes) {
let iconsCancellableToken: CancellableToken?
let progressClosure: ProgressBlock = { response in
}
let activityChange: (_ change: NetworkActivityChangeType) -> Void = { (activity) in
}
let cmsCommonRequestType=self.getCmsCommonRequestType(type: type)
iconsCancellableToken = CMSProvider<CMSCommonResponse>.request( .cmsCommonRequest(request: cmsCommonRequestType), success: { (_response) in
Log.b("CMSIcons: Get new icons from server for type: \(type)")
//Set http to https:
var iconsHostname:String=""{
didSet {
if let comps=URLComponents(string: iconsHostname) {
var _comps=comps
_comps.scheme = "https"
if let https = _comps.string {
iconsHostname=https
}
}
}
}
if (_response.data?.properties != nil) {
if _response.status {
let alias = self.getCmsAlias(type: type)
let property = _response.data?.properties.filter {$0.alias?.lowercased()==ValueHelper.getCMSAlias(alias)}.first?.value
if let jsonStr = property {
iconsHostname = _response.data?.hostName ?? ""
if let obj:CMSValuesResponse = CMSValuesResponse.map(JSONString: jsonStr) {
if let fieldsets=obj.fieldsets {
if fieldsets.count > 0 {
for index in 1...fieldsets.count {
let element=fieldsets[index-1]
if let prop = element.properties {
if(prop.count > 0) {
let urlAlias = self.getCmsURLAlias(type: type)
let iconUrl = prop.filter {$0.alias?.lowercased()==ValueHelper.getCMSAlias(urlAlias)}.first?.value
let name = prop.filter {$0.alias?.lowercased()==ValueHelper.getCMSAlias(.iconsNameAlias)}.first?.value
if let iconUrl=iconUrl, let name=name {
if let url = URL(string: iconsHostname+iconUrl) {
DispatchQueue.global().async {
if let data = try? Data(contentsOf: url) {
Log.b("CMSIcons: Icon url \(url.absoluteString) to Data done.")
var databaseDumpObject=CMSIconStructure()
databaseDumpObject.name=name
databaseDumpObject.data=data
self.dumpIconToLocalStorage(object: databaseDumpObject, type: type)
}
}
}
}
}
}
}//Loop ends.
//After success:
self.setFetchIconsDateStamp(type:type)
}
}
}
}
}
}
}, error: { (error) in
}, failure: { (_) in
}, progress: progressClosure, activity: activityChange) as? CancellableToken
}
//Dump icon data into CoreData:
func dumpIconToLocalStorage(object: CMSIconStructure, type: CMSIconsTypes) {
let entityName = self.getEntityName(type: type)
if #available(iOS 10.0, *) {
Log.b("Do CoreData task in background thread")
//Do CoreData task in background thread:
let context = appDelegate().persistentContainer.viewContext
let privateContext: NSManagedObjectContext = {
let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
moc.parent = context
return moc
}()
//1: Read all offline Icons:
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
fetchRequest.predicate = NSPredicate(format: "name = %#",
argumentArray: [object.name.lowercased()])
do {
let results = try privateContext.fetch(fetchRequest) as? [NSManagedObject]
if results?.count != 0 {
//2: Icon already found in CoreData:
if let icon=results?[0] {
icon.setValue(object.name.lowercased(), forKey: "name") //save lowercased
icon.setValue(object.data, forKey: "data")
}
} else {
//3: Icon not found in CoreData:
let entity = NSEntityDescription.entity(forEntityName: entityName, in: privateContext)
let newIcon = NSManagedObject(entity: entity!, insertInto: privateContext)
newIcon.setValue(object.name.lowercased(), forKey: "name") //save lowercased
newIcon.setValue(object.data, forKey: "data")
}
Log.b("CMSIcons: Icon data saved locally against name: \(object.name)")
} catch {
Log.i("Failed reading CoreData \(entityName.uppercased()). Error: \(error)")
}
privateContext.perform {
// Code in here is now running "in the background" and can safely
// do anything in privateContext.
// This is where you will create your entities and save them.
do {
try privateContext.save()
} catch {
Log.i("Failed reading CoreData \(entityName.uppercased()). Error: \(error)")
}
}
} else {
// Fallback on earlier versions
}
}
}
It is generally not recommended to store images as binary data in a Core Data persisted store. In stead, write the images to a local directory and store the local URL in core data instead. Here's an example workflow that might simplify some of your issues that you are experiencing using this recommended approach:
class IconsHelper {
let container: NSPersistentContainer
let provider: CMSProvider<CMSCommonResponse>
private let queue: DispatchQueue = DispatchQueue(label: "IconsHelper", qos: .userInitiated)
let documentsDirectory: URL = {
let searchPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
guard let path = searchPath.last else {
preconditionFailure("Unable to locate users documents directory.")
}
return URL(fileURLWithPath: path)
}()
init(container: NSPersistentContainer, provider: CMSProvider<CMSCommonResponse>) {
self.container = container
self.provider = provider
}
enum Icon: String, Hashable {
case amc
case bank
}
func getIcons(_ icons: Set<Icon>, dispatchQueue: DispatchQueue = .main, completion: #escaping ([Icon: URL]) -> Void) {
queue.async {
var results: [Icon: URL] = [:]
guard icons.count > 0 else {
dispatchQueue.async {
completion(results)
}
return
}
let numberOfIcons = icons.count
var completedIcons: Int = 0
for icon in icons {
let request = [""] // Create request for the icon
self.provider.request(request) { (result) in
switch result {
case .failure(let error):
// Do something with the error
print(error)
completedIcons += 1
case .success(let response):
// Extract information from the response for the icon
let imageData: Data = Data() // the image
let localURL = self.documentsDirectory.appendingPathComponent(icon.rawValue + ".png")
do {
try imageData.write(to: localURL)
try self.storeURL(localURL, forIcon: icon)
results[icon] = localURL
} catch {
print(error)
}
completedIcons += 1
if completedIcons == numberOfIcons {
dispatchQueue.async {
completion(results)
}
}
}
}
}
}
}
func storeURL(_ url: URL, forIcon icon: Icon) throws {
// Write the local URL for the specific icon to your Core Data Container.
let context = container.newBackgroundContext()
// Locate & modify, or create CMSIconStructure using the context above.
try context.save()
}
}
Then in your homepage view controller:
// Display Animation
let helper: IconsHelper = IconsHelper.init(container: /* NSPersistentContainer */, provider: /* API Provider */)
helper.getIcons([.amc, .bank]) { (results) in
// Process Results
// Hide Animation
}
The general design here to have a single call that will handle the downloading & processing of the images, then respond with the results after all of the networking calls & core data interactions have finished.
In the example, you initialize your IconsHelper with a reference to the CoreData NSPersistentContainer, and your networking instance. Does this approach help to clarify why your example code doesn't work the way you're expecting?
The app uses Share Extension to import a string of .txt files to Core Data. And then syncs the Core Data to iCloud. There is an entity called Item. When sharing a new item in system Files through Share Extension, the code needs to calculate the order for the new item to be imported. And the code is:
import Foundation
import CoreData
#objc(Item)
public class Item: NSManagedObject, Identifiable {
class func nextOrder() -> Int {
let keyPathExpression = NSExpression.init(forKeyPath: "order")
let maxNumberExpression = NSExpression.init(forFunction: "max:", arguments: [keyPathExpression])
let expressionDescription = NSExpressionDescription()
expressionDescription.name = "maxNumber"
expressionDescription.expression = maxNumberExpression
expressionDescription.expressionResultType = .decimalAttributeType
var expressionDescriptions = [AnyObject]()
expressionDescriptions.append(expressionDescription)
// Build out our fetch request the usual way
let request: NSFetchRequest<NSFetchRequestResult> = Item.fetchRequest()
request.resultType = .dictionaryResultType
request.propertiesToFetch = expressionDescriptions
request.predicate = nil
// Our result should to be an array of dictionaries.
var results: [[String:AnyObject]]?
do {
results = try CoreData.stack.context.fetch(request) as? [[String:NSNumber]] <-- errors here, Exception: "executeFetchRequest:error: <null> is not a valid NSFetchRequest."
if let maxNumber = results?.first!["maxNumber"] {
// Return one more than the current max order
return maxNumber.intValue + 1
} else {
// If no items present, return 0
return 0
}
} catch _ {
// If any failure, just return default
return 0
}
}
}
The code of Share Extension is:
import UIKit
import Social
import CoreServices
import CoreData
class ShareViewController: SLComposeServiceViewController {
var item = [Item]()
var newItem: Item?
override func isContentValid() -> Bool {
return true
}
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
if let inputItem = extensionContext!.inputItems.first as? NSExtensionItem {
if let itemProvider = inputItem.attachments?.first {
// This line was missing
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
itemProvider.loadItem(forTypeIdentifier: kUTTypeText as String) { (urlItem, error) in
if let filePathURL = urlItem as? URL {
do {
let nextOrder = Item.nextOrder() <-- errors here, Exception: "executeFetchRequest:error: <null> is not a valid NSFetchRequest."
// Some operation to import new item
// useless code for this question
// ...
} catch {
print("error")
}
}
}
}
}
}
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "iCloud.com.xxxxx")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
The Share Extension works well in the first time sharing. After the sharing, a new item is added in the Core Data.
But it will crash when sharing files the second time, saying Exception: "executeFetchRequest:error: <null> is not a valid NSFetchRequest."
The code errors is in the func nextOrder(), and the line is results = try CoreData.stack.context.fetch(request) as? [[String:NSNumber]] which has been commented in the code above.
A similar question is here. But is my bug the same as it? And I did not solve it following the answers in the question.
I am trying to add some unit tests for my Core Data code. But I always have this issue, the first test always run correctly, but the second one crashes because entity name is nil.
I also get this error:
Multiple NSEntityDescriptions claim the NSManagedObject subclass 'Gym.Exercise' so +entity is unable to disambiguate.
Failed to find a unique match for an NSEntityDescription to a managed object subclass
So my guess is that I am not doing something right in tearDown().
override func setUp() {
super.setUp()
coreDataStack = CoreDataStack(storeType: .inMemory)
context = coreDataStack.context
}
override func tearDown() {
coreDataStack.reset()
context = nil
super.tearDown()
}
Here is my CoreDataStack class:
final class CoreDataStack {
var storeType: StoreType!
public init(storeType: StoreType) {
self.storeType = storeType
}
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Gym")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Unresolved error \(error), \(error.localizedDescription)")
} else {
description.type = self.storeType.type
}
}
return container
}()
public var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
public func reset() {
guard let store = persistentContainer.persistentStoreCoordinator.persistentStores.first else { fatalError("No store found")}
guard let url = store.url else { fatalError("No store URL found")}
try! FileManager.default.removeItem(at: url)
NSPersistentStoreCoordinator.destroyStoreAtURL(url: url)
}
}
And the definition to destroyStoreAtURL:
extension NSPersistentStoreCoordinator {
public static func destroyStoreAtURL(url: URL) {
do {
let psc = self.init(managedObjectModel: NSManagedObjectModel())
try psc.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType, options: nil)
} catch let e {
print("failed to destroy persistent store at \(url)", e)
}
}
}
I was using this code in the past for unit testing and it works, the difference is that in the past when I setup the NSManagedObject classes in editor I used the following config:
Module - Global namespace
Codegen - Class Definition
Now I use:
Module - Current Product Module
Codegen - Manual/None
Because I want to add my classes manually.
So does anyone know why the behavior is different now ?
edit - my NSManagedObject extension helper (the error occurs inside the first line of fetch() method when trying to retrieve the entity name):
extension Managed where Self: NSManagedObject {
public static var entityName: String {
return entity().name!
}
public static func fetch(in context: NSManagedObjectContext, configurationBlock: (NSFetchRequest<Self>) -> () = { _ in }) -> [Self] {
let request = NSFetchRequest<Self>(entityName: Self.entityName)
configurationBlock(request)
return try! context.fetch(request)
}
public static func count(in context: NSManagedObjectContext, configure: (NSFetchRequest<Self>) -> () = { _ in }) -> Int {
let request = NSFetchRequest<Self>(entityName: entityName)
configure(request)
return try! context.count(for: request)
}
public static func findOrFetch(in context: NSManagedObjectContext, matching predicate: NSPredicate) -> Self? {
guard let object = materializeObject(in: context, matching: predicate) else {
return fetch(in: context) { request in
request.predicate = predicate
request.returnsObjectsAsFaults = false
request.fetchLimit = 1
}.first
}
return object
}
public static func materializeObject(in context: NSManagedObjectContext, matching predicate: NSPredicate) -> Self? {
for object in context.registeredObjects where !object.isFault {
guard let result = object as? Self, predicate.evaluate(with: result) else {
continue
}
return result
}
return nil
}
public static func findOrCreate(in context: NSManagedObjectContext, matching predicate: NSPredicate, configure: (Self) -> ()) -> Self {
guard let object = findOrFetch(in: context, matching: predicate) else {
let newObject: Self = context.insertObject()
configure(newObject)
return newObject
}
return object
}
}
You should try deleting all the found instances of persistentStore instead of deleting the first.
try replacing the reset function with below:
public func reset() {
let stores = persistentContainer.persistentStoreCoordinator.persistentStores
guard !stores.isEmpty else {
fatalError("No store found")
}
stores.forEach { store in
guard let url = store.url else { fatalError("No store URL found")}
try! FileManager.default.removeItem(at: url)
NSPersistentStoreCoordinator.destroyStoreAtURL(url: url)
}
}
And see if you still have the issue.
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
}
Just updated to Xcode 7.0 from Xcode 6.4. In my project I am getting an error now which I try to solve the whole night and did not get it.
The error message is: Initializer for conditional binding must have optional type not 'nsmanagedobjectcontext'
The error is coming twice in lines if let managedObjectContext = self.managedObjectContext { in following code
func preloadData () {
// Retrieve data from the source file
if let contentsOfURL = NSBundle.mainBundle().URLForResource("listofdata", withExtension: "csv") {
// Remove all the items before preloading
removeData()
var error:NSError?
if let items = parseCSV(contentsOfURL, encoding: NSUTF8StringEncoding, error: &error) {
// Preload the items
if let managedObjectContext = self.managedObjectContext {
for item in items {
let listOfItem = NSEntityDescription.insertNewObjectForEntityForName("ListOfItem", inManagedObjectContext: managedObjectContext) as! ListOfItem
listOfItem.name = item.name
listOfItem.address = item.address
listOfItem.phone = item.phone
if managedObjectContext.save(&error) != true {
print("insert error: \(error!.localizedDescription)")
}
}
}
}
}
}
func removeData () {
// Remove the existing items
if let managedObjectContext = self.managedObjectContext {
let fetchRequest = NSFetchRequest(entityName: "ListOfItem")
var e: NSError?
let listOfItems = managedObjectContext.executeFetchRequest(fetchRequest, error: &e) as! [ListOfItem]
if e != nil {
print("Failed to retrieve record: \(e!.localizedDescription)")
} else {
for listOfItem in listOfItems {
managedObjectContext.deleteObject(listOfItem)
}
}
}
}
Appreciate all help! Thanks!
Update:
The updated code looks like this, but still have these two errors in the first function preloadData:
Missing argument for parameter 'error' in call
Initializer for conditional binding must have Optional type, not 'NSManagedObjectContext'
func preloadData () {
// Retrieve data from the source file
if let contentsOfURL = NSBundle.mainBundle().URLForResource("listofdata", withExtension: "csv") {
// Remove all the menu items before preloading
removeData()
do {
let items = try parseCSV(contentsOfURL, encoding: NSUTF8StringEncoding)
// Preload the items
if let managedObjectContext = self.managedObjectContext {
for item in items {
let listOfItem = NSEntityDescription.insertNewObjectForEntityForName("ListOfItem", inManagedObjectContext: managedObjectContext) as! ListOfItem
listOfItem.name = item.name
listOfItem.address = item.address
listOfItem.phone = item.phone
if managedObjectContext.save() != true {
print("insert error: \(error.localizedDescription)")
}
}
}
} catch let error as NSError {
print("insert error: \(error.localizedDescription)")
}
}
This function shows no errors
func removeData () {
// Remove the existing items
let fetchRequest = NSFetchRequest(entityName: "ListOfItem")
do {
let listOfItems = try self.managedObjectContext.executeFetchRequest(fetchRequest) as! [ListOfItem]
for listOfItem in listOfItems {
self.managedObjectContext.deleteObject(listOfItem)
}
}
catch let error as NSError {
print("Failed to retrieve record: \(error.localizedDescription)")
}
Can some help? Thanks!
In Swift 2 the Core Data template implements the property managedObjectContext in AppDelegate as non-optional. Probably the updater changed the implementation accordingly.
The benefit is that the optional bindings are not necessary any more, but you have to consider the new error handling for example
func removeData () {
let fetchRequest = NSFetchRequest(entityName: "ListOfItem")
do {
let listOfItems = try self.managedObjectContext.executeFetchRequest(fetchRequest) as! [ListOfItem]
for listOfItem in listOfItems {
self.managedObjectContext.deleteObject(listOfItem)
}
}
catch let error as NSError {
print("Failed to retrieve record: \(error.localizedDescription)")
}
}
try below code
removeData()
func removeData () {
let fetchRequest = NSFetchRequest(entityName: "MenuItem")
do {
let listOfItems = try self.managedObjectContext.executeFetchRequest(fetchRequest) as! [MenuItem]
for listOfItem in listOfItems {
self.managedObjectContext.deleteObject(listOfItem)
}
}
catch let error as NSError {
print("Failed to retrieve record: \(error.localizedDescription)")
}
}
preloadData ()
func preloadData () {
// Retrieve data from the source file
if let contentsOfURL = NSBundle.mainBundle().URLForResource("menudata", withExtension: "csv") {
// Remove all the menu items before preloading
removeData()
var error:NSError?
if let items = parseCSV(contentsOfURL, encoding: NSUTF8StringEncoding, error: &error) {
// Preload the menu items
for item in items {
let menuItem = NSEntityDescription.insertNewObjectForEntityForName("MenuItem", inManagedObjectContext:self.managedObjectContext) as! MenuItem
menuItem.name = item.name
menuItem.detail = item.detail
menuItem.price = (item.price as NSString).doubleValue
}
}
}
}