Download multiple images into CoreData - ios

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?

Related

coredata update json background thread problem

I have the following coredata singleton class
in my appdelegate i try to update the data from json
but in my app when it launches i get different error messages regarding thread
like
Main Thread Checker: UI API called
or observer of NSManagedObjectContextObjectsDidChangeNotification
or Incorrect guard value
What is the problem and what changes should i make for it to work?
thanks
import CoreData
import Foundation
class CoreDataStack {
static let sharedManager = CoreDataStack()
private init() {} // Prevent clients from creating another instance.
//This is the name of your coreData Database
static let modelName = "myDB"
static let FirstLaunchKey = "firstLaunch"
lazy var managedObjectModel: NSManagedObjectModel = {
let modelURL = Bundle.main.url(forResource: CoreDataStack.modelName, withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
lazy var applicationDocumentsDirectory: URL = {
return NSPersistentContainer.defaultDirectoryURL()
}()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let persistentStoreURL = self.applicationDocumentsDirectory.appendingPathComponent(CoreDataStack.modelName + ".sqlite")
do {
// let dict = [NSSQLitePragmasOption: ["journal_mode":"DELETE"]]
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: persistentStoreURL,
options: [NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: false])
} catch {
fatalError("Persistent store error! \(error)")
}
return coordinator
}()
fileprivate lazy var saveManagedObjectContext: NSManagedObjectContext = {
let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
moc.persistentStoreCoordinator = self.persistentStoreCoordinator
return moc
}()
#objc lazy var mainObjectContext: NSManagedObjectContext = {
let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.parent = self.saveManagedObjectContext
return managedObjectContext
}()
func saveMainContext() {
guard mainObjectContext.hasChanges || saveManagedObjectContext.hasChanges else {
return
}
mainObjectContext.performAndWait() {
// print("save performAndWait")
do {
try self.mainObjectContext.save()
} catch {
fatalError("Error saving main managed object context! \(error)")
}
}
saveManagedObjectContext.perform() {
// print("save simple")
do {
try self.saveManagedObjectContext.save()
} catch {
fatalError("Error saving private managed object context! \(error)")
}
}
}
func removeDatabaseForVersion(version:String){
let previouslyVersion = UserDefaults.standard.bool(forKey: version)
if !previouslyVersion {
UserDefaults.standard.set(true, forKey: version)
// Default directory where the CoreDataStack will store its files
let directory = NSPersistentContainer.defaultDirectoryURL()
let url = directory.appendingPathComponent(CoreDataStack.modelName + ".sqlite")
let shmURL = directory.appendingPathComponent(CoreDataStack.modelName + ".sqlite-shm")
let walURL = directory.appendingPathComponent(CoreDataStack.modelName + ".sqlite-wal")
_ = try? FileManager.default.removeItem(at: url)
_ = try? FileManager.default.removeItem(at: shmURL)
_ = try? FileManager.default.removeItem(at: walURL)
}
}
}
in my appdelegate:
UpdateDbClass.updateDatabase(entityName: DbTable.VehiclesEntity.rawValue, completionHandler: {
print(" DB updated delegate")
})
in updatedb class:
import UIKit
import Alamofire
import CoreData
enum LoaderError:String{
case
JsonFailed,
PathFailed,
NoEntityDescription,
UnknownError
}
enum DbTable:String{
case
VehiclesEntity,
PhotosEntity,
ModelsEntity,
NewsEntity,
StylesEntity
}
class UpdateDbClass {
static func updateDatabase(entityName:String,completionHandler: #escaping () -> Void){
var url = URL(string: UrlRepository.VehiclesJsonUrl!)!
var table = ""
switch entityName {
case DbTable.VehiclesEntity.rawValue:
table = "Vehicles"
url = URL(string: UrlRepository.VehiclesJsonUrl!)!
case DbTable.PhotosEntity.rawValue:
table = "Photos"
url = URL(string: UrlRepository.PhotosJsonUrl!)!
table = "Styles"
url = Bundle.main.url(forResource: "Styles", withExtension: "json")!
// url = URL(string: UrlRepository.NewsJsonUrl!)!
default:
break
}
let uuid = UUID().uuidString
let parameters: Parameters = [
"id": uuid
]
let queue = DispatchQueue(label: "com.my.test", qos: .background, attributes: .concurrent)
AF.request(url, method: .get, parameters: parameters, encoding: URLEncoding(destination: .queryString), headers: nil).responseJSON(queue:queue){ response in
switch response.result {
case let .success(value):
if let items = value as? [[String: Any]] {
var itemsArray:[Int32] = []
for item in items{
if let id = item["id"] as? Int32{
itemsArray.append(id)
}
}
guard let entity = NSEntityDescription.entity(forEntityName: table, in:(CoreDataStack.sharedManager.mainObjectContext)) else {
fatalError("Could not find entity descriptions!")
}
switch entityName {
case DbTable.StylesEntity.rawValue: //Styles
checkDeletedRecords(jsonItems: itemsArray,table: table)
for item in items{
guard let id = item["id"] as? Int32 else {return}
//Check if not exists
if !CheckIfExists(id: id,table:table){
print("id \(id) does not exist")
//Insert Record
let object = Styles(entity: entity, insertInto: CoreDataStack.sharedManager.mainObjectContext)
object.setValue(item["id"], forKey: "id")
object.setValue(item["style"] as! String, forKey: "style")
object.setValue(item["image"] as! String, forKey: "image")
CoreDataStack.sharedManager.saveMainContext()
}
else{ //Update Record
// print("id \(item["id"]) exists")
do{
let fetchRequest = NSFetchRequest<Styles>(entityName:"Styles")
let predicate = NSPredicate(format: "id == %d",item["id"] as! Int32)
fetchRequest.predicate = predicate
let req = try CoreDataStack.sharedManager.mainObjectContext.fetch(fetchRequest)
let object = req[0] as NSManagedObject
object.setValue(item["style"] as! String, forKey: "style")
object.setValue(item["image"] as! String, forKey: "image")
CoreDataStack.sharedManager.saveMainContext()
}catch{
print("there was an error")
}
completionHandler()
}
}
break;
default:
break
}
}
break
case let .failure(error):
print(error as NSError)
break
}
}
}
}
protocol CoreDataWorkerProtocol {
associatedtype EntityType
}
enum VoidResult {
case success
case failure(NSError)
}
Response json is called on background thread and you are trying to use viewContext in background thread.
You should use perform block if you want to use viewContext in background.
For example like this
AF.request(:).responseJSON(queue:queue){ response in
let mainContext = CoreDataStack.sharedManager.mainObjectContext
mainContext.perform {
guard let entity = NSEntityDescription.entity(forEntityName: table, in: mainContext) else {
fatalError("Could not find entity descriptions!")
}
}
}

Closure returning data before async work is done

UPDATED WITH PROPOSED SOLUTION AND ADDITIONAL QUESTION
I'm officially stuck and also in callback hell. I have a call to Firebase retrieving all articles in the FireStore. Inside each article object is a an Image filename that translates into a storage reference location that needs to be passed to a function to get the absolute URL back. I'd store the URL in the data, but it could change. The problem is the ArticleListener function is prematurely returning the closure (returnArray) without all the data and I can't figure out what I'm missing. This was working fine before I added the self.getURL code, but now it's returning the array back empty and then doing all the work.
If anyone has some bonus tips here on chaining the methods together without resorting to PromiseKit or GCD that would be great, but open to all suggestions to get this to work as is
and/or refactoring for more efficiency / readability!
Proposed Solution with GCD and updated example
This is calling the Author init after the Article is being created. I am trying to transform the dataDict dictionary so it get's used during the Author init for key ["author"]. I think I'm close, but not 100% sure if my GCD enter/leave calls are happening in the right order
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
print("Error in setting up snapshot listener - \(error)")
} else {
let fireStoreDispatchGrp = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
//NEW EXAMPLE WITH ADDITIONAL TASK HERE
if let author = $0.data()["author"] as? DocumentReference {
author.getDocument() {(authorSnapshot, error) in
fireStoreDispatchGrp.enter() //1
if let error = error {
print("Error getting Author from snapshot inside Article getDocumentFunction - leaving dispatch group and returning early")
fireStoreDispatchGrp.leave()
return
}
if let newAuthor = authorSnapshot.flatMap(Author.init) {
print("Able to build new author \(newAuthor)")
dataDict["author"] = newAuthor
dataDict["authorId"] = authorSnapshot?.documentID
print("Data Dict successfully mutated \(dataDict)")
}
fireStoreDispatchGrp.leave() //2
}
}
///END OF NEW EXAMPLE
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
fireStoreDispatchGrp.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
dataDict["image"] = url.absoluteString
case .failure(let error):
print("Error getting URL for author: \n Error: \(error) \n forReference: \(reference) \n forArticleID: \(id)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
returnArray.append(newArticle)
}
fireStoreDispatchGrp.leave() ///3
}
}
}
//Completion block
print("Exiting dispatchGroup all data should be setup correctly")
fireStoreDispatchGrp.notify(queue: .main) { ///4
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
Original Code
Calling Setup Code
self.manager.SetupArticleListener() { [weak self] articles in
print("🌈🌈🌈🌈🌈🌈🌈In closure function to update articles🌈🌈🌈🌈🌈🌈🌈")
self?.articles = articles
}
Article Listener
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
}
}
}
print("🌈🌈🌈🌈🌈🌈🌈 sending back completion array \(returnArray)🌈🌈🌈🌈🌈🌈🌈")
completion(returnArray)
}
}
updateListeners(for: listener)
}
GetURL
private func getURL(reference: StorageReference, _ result: #escaping (Result<URL, Error>) -> Void) {
reference.downloadURL() { (url, error) in
if let url = url {
result(.success(url))
} else {
if let error = error {
print("error")
result(.failure(error))
}
}
}
}
You need dispatch group as the for loop contains multiple asynchronous calls
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
let g = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
g.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
g.leave() /// 3
}
}
}
g.notify(queue:.main) { /// 4
print("🌈🌈🌈🌈🌈🌈🌈 sending back completion array \(returnArray)🌈🌈🌈🌈🌈🌈🌈")
completion(returnArray)
}
}
}
updateListeners(for: listener)
}

Problems with CoreData

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)

CoreData returns empty data

coreData returns empty data when there should not be any, even if you uninstall the application and reinstall it and make a request to Π‘ore Data, the context.fetch returns the data
get all Data in Π‘ore Data
func getMyLoadBook(){
words.removeAll()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let fetchRequest:NSFetchRequest<Favorite> = Favorite.fetchRequest()
fetchRequest.returnsObjectsAsFaults = false
do {
let result = try! context.fetch(fetchRequest)
print(result)
if result.isEmpty {
emptyBookMark()
return
} else {
tableView.isHidden = false
}
for data in result as [NSManagedObject] {
if let _ = data.value(forKey: "word"){
let initData = Words(word: (data.value(forKey: "word") as? [String]) ?? [""], wordDesc: (data.value(forKey: "wordDesc") as? [String]) ?? nil, translation: (data.value(forKey: "translation") as? [String]) ?? [""], translDesc: (data.value(forKey: "translDesc") as? [String]) ?? nil)
words.append(initData)
}
}
}
tableView.reloadData()
}
I have these functions, but they are not called when I get data from coreData
// creates a path and checks for the presence of an element
static func coreDataResult(data: [[String?]?]?, completion: #escaping (NSFetchRequest<NSFetchRequestResult>, Favorite?, NSManagedObjectContext) -> ()){
guard let w = data?.first, let word = w, let t = data?.last, let transl = t else { return }
DispatchQueue.main.async {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
guard let entity = NSEntityDescription.entity(forEntityName: "Favorite", in: context) else { return }
guard let taskObject = NSManagedObject(entity: entity, insertInto: context) as? Favorite else { return }
let predicate = NSPredicate(format: "word == %#", word)
let predicate2 = NSPredicate(format: "translation == %#", transl)
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Favorite")
let andPredicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, predicate2])
fetchRequest.predicate = andPredicate
completion(fetchRequest, taskObject, context)
}
}
// remove data from Π‘ore Data
static func deleteFromCoreData(data: [[String?]?]?){
coreDataResult(data: data, completion: { (result, taskObject, context) in
do {
let fetchedEntities = try context.fetch(result) as! [Favorite]
if let entityToDelete = fetchedEntities.first {
context.delete(entityToDelete)
}
do {
try context.save()
if let data = getDataFromContext(result:fetchedEntities){
Analytics.logEvent("RemovedFavorite", parameters: ["word": data.0, "translation": data.1])
YMMYandexMetrica.reportEvent("RemovedFavorite", parameters: ["word": data.0, "translation": data.1], onFailure: nil)
}
} catch {
print(error)
}
} catch { print(error) }
})
}
// add data to Π‘ore Data
static func saveWithModelToCoreData(_ words: Words){
DispatchQueue.main.async {
coreDataResult(data: [words.word, words.translation], completion: { (result, taskObject, context) in
do {
let fetchedEntities = try context.fetch(result) as! [Favorite]
if let _ = fetchedEntities.first?.word {
print("the element already have in coreData")
} else {
taskObject?.setValue(words.word, forKey: "word")
taskObject?.setValue(words.translation, forKey: "translation")
taskObject?.setValue(words.descript, forKey: "wordDesc")
taskObject?.setValue(words.translDesc, forKey: "translDesc")
do {
try context.save()
} catch {
print(error)
}
}
} catch {
print(error)
}
})
}
}
that's what result returns
[<Favorite: 0x283478500> (entity: Favorite; id: 0x281306ee0 <x-coredata:///Favorite/t722DD7F9-8DD7-4AC4-AA20-02324AB1B08713> ; data: {
translDesc = nil;
translation = nil;
word = nil;
wordDesc = nil;
})
It seems that you are you using a simple core-data setup, where all read and write are done on the main thread to the viewContext. This setup is fine for simple application where you don't expect to do a bulk import, or have a huge amount of entities. It should simplify a lot of multithread issues so I am a little confused why you have such a complex setup with callbacks and DispatchQueue.main.async when everything should just simply run on the main thread. (Perhaps you are planing for a future with a more complex setup?).
In any event, one of the consequences of this is that any changes to the viewContext will appear in your app for the lifetime of the app, even if you don't call save. This is because there is a single context - so even it is not saved, it has still been changed.
In the method coreDataResult you create an empty object, and then in saveWithModelToCoreData it is either set with values and the context saved or it is found to already exist and no further action is taken. If coreDataResult returned on a background context that would be fine. The empty object would disappear when the background context. The problem is that you are writing to the viewContext so the context does not go away, and the object sticks around.
If the application would quit right then, you wouldn't see it in the next launch. But if save is called any time after, then the empty object will also be saved.
I would suggest not creating objects unless you already know that you want them. I would refactor so that there is a single function that checks for duplicate, and then creates and set or does nothing. As it is I don't see the value of the two different methods.

Loading saved context from AppDelegate to another ViewController

I'm trying to make an array from my Viewcontroller equal to, the objects my core data has saved. I'm using core data and created an entity named Pokemon which has 3 attributes name, id and generation. In the app delegate, I use the following function to get Pokemon from this API. This is what I do to parse the data and save the context:
typealias DownloadCompleted = () -> ()
var pokemonId: Int16 = 0
func fetchPokemon(url: String, completed: #escaping DownloadCompleted) {
let context = coreData.persistentContainer.viewContext
let url = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: url) { (data, repsonse, error) in
if error != nil {
print(error!)
}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! NSDictionary
let jsonArray = jsonResult.value(forKey: "results") as! [[String: Any]]
for pokemonData in jsonArray {
self.pokemonId += 1
if self.pokemonId > 721 {
self.coreData.saveContext()
return
}
guard let name = pokemonData["name"] as? String else {
return
}
let pokemon = Pokemon(context: context)
pokemon.name = name
pokemon.id = self.pokemonId
print("Name: \(pokemon.name) Id:\(self.pokemonId)")
if self.pokemonId <= 151 {
pokemon.generation = 1
} else if self.pokemonId <= 251 {
pokemon.generation = 2
} else if self.pokemonId <= 386 {
pokemon.generation = 3
} else if self.pokemonId <= 493 {
pokemon.generation = 4
} else if self.pokemonId <= 649 {
pokemon.generation = 5
} else if self.pokemonId <= 721 {
pokemon.generation = 6
}
}
guard let nextURL = jsonResult.value(forKey: "next") as? String else {
self.coreData.saveContext()
return
}
DispatchQueue.main.async {
self.fetchPokemon(url: nextURL, completed: {
self.coreData.saveContext()
})
completed()
}
} catch let err {
print(err.localizedDescription)
}
}
task.resume()
}
This is how I call it in the appDelegate. Really don't know what to do in the middle of the fetchPokemon or how to call it in another view controller. So I left it blank, not sure if this has something to do with the problem I'm having.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let context = self.coreData.persistentContainer.viewContext
let pokemonListVC = self.window?.rootViewController as! PokemonListVC
pokemonListVC.context = context
fetchPokemon(url: pokemonAPI) {
}
return true
}
Im using this SQL-Light read-only app from the app store. I check the data and all 721 pokemon are saving. Now, I don't know how I would be able to make the array in my view controller equal to all 721 Pokemon saved. I added this code into my viewController.
class PokemonListVC: UIViewController {
weak var context: NSManagedObjectContext! {
didSet {
return pokemon = Pokemon(context: context)
}
}
var pokemon: Pokemon? = nil
lazy var pokemons = [Pokemon]()
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}
func loadData() {
pokemons = pokemon!.loadPokemon(generation: 1, context: context)
}
}
I've created an extension of my Pokemon entity and added a function loadPokemon that filters the Pokemon by generation. Here is the code.
extension Pokemon {
func loadPokemon(generation: Int16 = 0, context: NSManagedObjectContext) -> [Pokemon] {
let request: NSFetchRequest<Pokemon> = Pokemon.fetchRequest()
request.predicate = NSPredicate(format: "generation = %#", generation)
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
do {
let pokemons = try context.fetch(request)
print("My Pokemon count: \(pokemons.count)")
return pokemons
} catch let err {
print(err.localizedDescription)
}
return []
}
}
When I call the loadData in my ViewController it crashes. The array count is 0 and so is the one in the hero extension. So I don't how to make my array equal the Pokemon saved from coreData.
Would really appreciate any help provided. :)
Here is my deleteRecords code, which is also in my appDelegate. This deletes all records when app launches. I call this method at the very beginning of didFinishLaunchingWithOption function before the fetchPokemons.
func deleteRecords() {
let context = coreData.persistentContainer.viewContext
let pokemonRequest: NSFetchRequest<Pokemon> = Pokemon.fetchRequest()
var deleteRequest: NSBatchDeleteRequest
var deleteResults: NSPersistentStoreResult
do {
deleteRequest = NSBatchDeleteRequest(fetchRequest: pokemonRequest as! NSFetchRequest<NSFetchRequestResult>)
deleteResults = try context.execute(deleteRequest)
} catch let err {
print(err.localizedDescription)
}
}
As you are saying that you have sure that all the pockemon records are stored correctly in your coredata you can simply fetch records from your codedata by providing fetch request. I have created demo for contact storing and I can get all the contact by this fetch request you can try this code in your ViewController where you want to fetch all the record.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject> (entityName: "Pokemon")
do {
arrPockemon = try managedContext.fetch(fetchRequest)
}catch let error as NSError {
showAlert(string: error.localizedDescription)
}
try to get all records first and if you get all then work for filtering extension and all. hope it will help you. you can learn from here https://code.tutsplus.com/tutorials/core-data-and-swift-core-data-stack--cms-25065
save flag on userDefault.
//check for first time when app is installed first time(first time flag is not present so)
let userDefault = UserDefaults.standard.dictionaryRepresentation()
if userDefault.keys.contains("isDataAvailable") {
//key is availebe so check it
if userDefault["isDataAvailable"] as! String == "1"{
//no need to call server for data
}else{
//fetch data from server
// once you get data from server make isDataAvailable flage as 1
UserDefaults.standard.setValue("1", forKey: "isDataAvailable")
UserDefaults.standard.synchronize()
}
}
else{
//flag is not avalable so call server for data
// once you get data from server make isDataAvailable flage as 1
UserDefaults.standard.setValue("1", forKey: "isDataAvailable")
UserDefaults.standard.synchronize()
}

Resources