hello i'm try to implement Core Data and swiftUI for my project.
I created an Entity called Aeroporti, which contain 2 attributes iataAPT : String and icaoAPT: string.
I want to save simple data on that.
I setup the app delegate like this:
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "testFirebaseCoreData")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
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)")
}
}
}
my content view list the field icaoAPT in the entity Aeroporti
import SwiftUI
import CoreData
struct ContentView: View {
#ObservedObject var dm : DataManager
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Aeroporti.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Aeroporti.icaoAPT, ascending: true)]) var aeroport : FetchedResults<Aeroporti> // crea la variabile che contiene le ricette
var body: some View {
List{
HStack{
// NOT WORKING IF USING DataMANAGER
// Button(action: {
// self.dm.provaSalva()
// }) {
// Text("save from manager")
// }
saveFromView // button save from view
}
ForEach(aeroport, id: \.self) { apt in
Text(apt.icaoAPT ?? "sconosciuto")
// .environment(\.managedObjectContext, self.managedObjectContext)
}
}
}
var saveFromView : some View {
Button(action: {
let apt = Aeroporti(context: self.managedObjectContext)
apt.iataAPT = "MFM"
apt.icaoAPT = "VMMC"
do {
try self.managedObjectContext.save()
print("airport saved.")
} catch {
print(error.localizedDescription)
}
}) {
Text("save fromview")
}
}
}
all working fine and the data is saved if I use the button (saveFromView) in the ContentView.
now, in order to better handle the data I have create a DataManager where I put the same code to save the data in the entity, and I try to lunch this code on the content view via a button (save from manager) but it doest work, i'm getting the error "The operation couldn’t be completed. (Foundation._GenericObjCError error 0.)" when it save.
import Combine
import SwiftUI
import CoreData
class DataManager: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#Environment(\.managedObjectContext) var managedObjectContext
func provaSalva() {
let apt = Aeroporti(context: managedObjectContext)
apt.iataAPT = "MFM"
apt.icaoAPT = "VMMC"
do {
try self.managedObjectContext.save()
print("airport saved.")
} catch {
print(error.localizedDescription)
}
}
}
the question is?? why.. is the same code... how can I solve this issue if I want to handle the save data not in the ContentView.
thanks for the help..
Give this a try. This is the implementation I use for my app.
class DataManager {
//MARK: - Setup
//Singleton object
static let defaults = DataManager()
private init() {}
//Managed object context
var moc: NSManagedObjectContext {
return (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
}
//Save context, if changes were made
func save() {
if moc.hasChanges {
do {
print("Sucess!!")
try moc.save()
} catch {
print("Error while saving managedObjectContext \(error)")
}
}
}
//MARK: - Insert
//What you want to save
func provaSalva() {
let apt = Aeroporti(context: moc)
apt.iataAPT = "MFM"
apt.icaoAPT = "VMMC"
save()
}
}
Your saveFromView:
var saveFromView : some View {
Button(action: {
DataManager.defaults.provaSalva()
}) {
Text("save fromview")
}
}
solved by passing the db to the function
func provaSalva(dbCore: NSManagedObjectContext) {
let apt = Aeroporti(context: dbCore)
apt.iataAPT = "MFM"
apt.icaoAPT = "VMMC"
do {
try dbCore.save()
print("airport saved.")
} catch {
print(error.localizedDescription)
}
}
Related
I want my swift code to print out the strings attributes. Right now when calling the function I am getting a runtime error at context. I just want to print out all of each string entry. I have added the function in question below.
Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
let appDelegate = UIApplication.shared.delegate as! AppDelegate //Singlton instance
var context:NSManagedObjectContext!
#objc func pressRight(){
let fetchRequest = NSFetchRequest<Place>(entityName: "Name")
do {
let result = try context.fetch(fetchRequest)
let nameArray = result.map{$0.name}
print(nameArray)
} catch {
print("Could not fetch \(error) ")
}
}
pic
select manual in code gen
then create custom class of place add to your project
You are using the wrong entity name "Name" instead of "Place"
import Foundation
import CoreData
class CoreDataManager {
static let shared = CoreDataManager()
private init() {}
lazy var coreDataStack = CoreDataStack(modelName: "Place")
func allNames() -> [String]? {
let request: NSFetchRequest<Place> = Place.fetchRequest()
do {
// Peform Fetch Request
let places = try coreDataStack.managedContext.fetch(request)
return places.map({$0.name})
} catch {
print("Unable to Fetch Workouts, (\(error))")
}
return nil
}
func allPlaces() -> [Place]? {
let request: NSFetchRequest<Place> = Place.fetchRequest()
do {
// Peform Fetch Request
let places = try coreDataStack.managedContext.fetch(request)
return places
} catch {
print("Unable to Fetch Workouts, (\(error))")
}
return nil
}
}
if you still getting error then before this initialize your context
managedObjectContext/context you force unwrapping it
add this stack class
import Foundation
import CoreData
class CoreDataStack {
private let modelName: String
lazy var managedContext: NSManagedObjectContext = {
return self.storeContainer.viewContext
}()
init(modelName: String) {
self.modelName = modelName
}
private lazy var storeContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: self.modelName)
container.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
print("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()
func saveContext() {
guard managedContext.hasChanges else {return}
do{
try managedContext.save()
} catch let error as NSError {
print("Unresolved error \(error), \(error.userInfo)")
}
}
func updateContext() {
do {
try managedContext.save()
} catch let error as NSError {
print("Unresolved error \(error), \(error.userInfo)")
}
}
func clearChange() {
managedContext.rollback()
}
}
then how to use it
in your view controller viewDidLoad() function or any other button tap action you can get your place names like this
override func viewDidLoad() {
super.viewDidLoad()
// here you get all names
let names = CoreDataManager.shared.allNames()
print(names)
let places = CoreDataManager.shared.allPlaces()
print(places)
let namesAgain = places.map({$0.name})
print(namesAgain)
}
I have an entity called Cart, but it seems I can't get it to save a new data from my viewmodel.
Persistence.swift
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
for _ in 0..<10 {
let newCart = Cart(context: viewContext)
newCart.menuName = "name"
newCart.menuImg = "img"
newCart.menuCode = "code"
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "Project")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
func save() {
let context = container.viewContext
if context.hasChanges {
print("Found Changes")
do {
try context.save()
} catch {
print("ERROR")
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
} else {
print("NO Changes")
}
}
}
MenuViewModel.swift
class MenuViewModel.swift: ObservableObject, MenuDetailService {
#Environment(\.managedObjectContext) var managedObjectContext
func addToCart() {
let newCart = Cart(context: managedObjectContext)
newCart.menuCode = menuCode
newCart.menuName = menuName
newCart.menuImg = menuImg
PersistenceController.shared.save()
}
}
ProjectApp.swift
#main
struct ProjectApp: App {
#Environment(\.scenePhase) var scenePhase
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}.onChange(of: scenePhase) { _ in
persistenceController.save()
}
}
}
The persistence controller func save() always print no changes, seems like I use different context? and I don't know how to use the context in the persistence controller to save the new data. How do I do this? Thank you very much.
//Remove the `.swift`
class MenuViewModel: ObservableObject, MenuDetailService {
//#Environment does not work well outside of a struct :View . Updates are unreliable
var managedObjectContext = PersistenceController.shared.container.viewContext
// You need to pass the variables somehow to the new object when you call the method from the `View`
func addToCart(menuCode: String, menuName: String, menuImg: UIImage) {
let newCart = Cart(context: managedObjectContext)
newCart.menuCode = menuCode
newCart.menuName = menuName
newCart.menuImg = menuImg
PersistenceController.shared.save()
}
}
I am not sure about that tutorial, I have not looked into it, but as I have used CoreData with SwiftUI, this was nothing I have seen:
let newCart = Cart(context: managedObjectContext)
newCart.menuCode = menuCode
newCart.menuName = menuName
newCart.menuImg = menuImg
PersistenceController.shared.save()
Rather what I would expect is that you replace the last line there with this:
do {
try managedObjectContext.save()
} catch {
print(error.localizedDescription)
}
In order to initiate the coredata stack in app delegate is not recommended as it will create issue in a multi threaded environment. Therefore, I created a separate class for coredata stack and then a separate class to handle operations. And in the Coredata stack class it crashes. What am I missing here ?
CoreData Stack Class
Code looks like this
class CoreDataManager {
static let sharedManager = CoreDataManager()
private init() {} //Singleton
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyDatabase")
container.loadPersistentStores(completionHandler: { (_, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
lazy var viewContext: NSManagedObjectContext = {
return self.persistentContainer.viewContext
}()
lazy var cacheContext: NSManagedObjectContext = {
return self.persistentContainer.newBackgroundContext()
}()
lazy var updateContext: NSManagedObjectContext = {
let _updateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
_updateContext.parent = self.viewContext
return _updateContext
}()
}
This is how I access it
class StorageManager: NSObject {
lazy var managedObjectContext: NSManagedObjectContext = {
return CoreDataManager.sharedManager.persistentContainer.viewContext
}()
lazy var privateMOC: NSManagedObjectContext = {
return CoreDataManager.sharedManager.updateContext
}()
private func synchronize(privateMOC: NSManagedObjectContext) {
do {
try privateMOC.save()
self.managedObjectContext.performAndWait {
do {
try self.managedObjectContext.save()
// "Saved to main context"
} catch {
print("Could not synchonize data. \(error), \(error.localizedDescription)")
}
}
} catch {
print("Could not synchonize data. \(error), \(error.localizedDescription)")
}
}
}
A sample operation would look like this
func deleteContact(contactID: String) {
let privateMOC = self.privateMOC
privateMOC.performAndWait {
let request: NSFetchRequest<Contact> = Contact.fetchRequest()
request.predicate = NSPredicate(format: "contact_id= %#", contactID)
let result = try? privateMOC.fetch(request)
for object in result! {
privateMOC.delete(object)
}
self.synchronize(privateMOC: privateMOC)
}
}
I have two pieces of code to decode json data (local and remote). Both work. I am also able to visualize my local data, however not the remote data. They are exactly the same (just the location of the json file and the imageUrl differs). In my TestView.swift code, which is used for both cases, I have given two comments that point to my problem.
My issue: How do I need to define testData:[Test] for the remote case, which is well defined for the local case?
What is missing? Please help. I am new to Xcode and SwiftUI so any help will be greatly appreciated.
P.S. Basically I am trying to build on two Swift Tutorials (on YouTube), i.e. Build a Complex UI with SwiftUI from Start to Finish, SwiftUI Fetching JSON and Image Data with BindableObject.
// Data.swift
import SwiftUI
import Combine
// 1.) This first piece of code decodes local json data and correctly visualize it
let testData:[Test] = load("test.json")
func load<T:Decodable>(_ filename:String, as type:T.Type = T.self) -> T {
let data:Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
// 2.) This 2nd piece of code decodes remote json data but does not visualize it
class testDatas: ObservableObject {
#Published var tests:[Test] = [Test]()
func getAllTests() {
let file = URLRequest(url: URL(string: "https://myurl/test.json")!)
let task = URLSession.shared.dataTask(with: file) { (data, _, error) in
guard error == nil else { return }
do {
let tests = try JSONDecoder().decode([Test].self, from: data!)
DispatchQueue.main.async {
self.tests = tests
print(tests)
}
} catch {
print("Failed To decode: ", error)
}
}
task.resume()
}
init() {
getAllTests()
}
init(tests: [Test]) {
self.tests = tests
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
// TestView.swift (used for both decode cases)
import SwiftUI
// testData needed here only for the remote case, but this might be wrong and the problem?
let testData:[Test] = [Test]()
struct TestView: View {
// testDatas needed here only for the remote case to decode json data)
#ObservedObject var fixer: testDatas = testDatas()
#EnvironmentObject var loader: ImageLoader
var categories:[String:[Test]] {
.init(
grouping: testData,
by: {$0.category.rawValue}
)
}
var body: some View {
NavigationView{
List (categories.keys.sorted(), id: \String.self) {key in TestRow(categoryName: "\(key).environmentObject(ImageLoader(with: key.imageUrl)) Tests".uppercased(), tests: self.categories[key]!)
.frame(height: 320)
.padding(.top)
.padding(.bottom)
}
.navigationBarTitle(Text("TEST"))
}
}
}
class ImageLoader:ObservableObject
{
#Published var data:Data = Data()
func getImage(imageURL:String) {
guard let test = URL(string: imageURL) else { return }
URLSession.shared.dataTask(with: test) { (data, response, error) in
DispatchQueue.main.async {
if let data = data {
self.data = data
}
}
print(data as Any)
}.resume()
}
init(imageURL:String) {
getImage(imageURL: imageURL)
}
}
struct TestView_Previews: PreviewProvider {
#ObservedObject var imageLoader: ImageLoader
init(test:String)
{
imageLoader = ImageLoader(imageURL: test)
}
static var previews: some View {
TestView()
}
}
With some help of #Josh Homann on a related question, I could solve my problem. All I had to do is to replace "grouping: testData" with "grouping: networkManager.tests" and by using "#ObservedObject var networkManager: NetworkManager = NetworkManager()". This makes the definition of testData redundant.
I'm trying to create a singleton class which works with an NSManagedObjectContext.
This is the class:
import Foundation
import CoreData
class PersistenceService{
init(){}
// MARK: - Core Data stack
static var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
static var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "frazeit")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
static func saveContext () {
let mainContext = persistentContainer.viewContext
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = mainContext
privateContext.perform {
if privateContext.hasChanges {
do {
try privateContext.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
}
In some occasion, it does not push changes into the persistent store, while the app is open the persistent container is changed but when I re-run the app changes are gone. What's the right way to save the changes into the persistent store.
This the class that does not work properly:
class func add(word: String, quotes:[Quotes], language: String){
for item in quotes {
if let phrase = item.phrase, let author = item.author {
let quote = CachedQuotes(context: PersistenceService.context)
quote.phrase = phrase
quote.date = Date() as NSDate
quote.keyword = word
quote.language = language
quote.author = author
PersistenceService.saveContext()
}
}
}
I call it to save quotes which are fetched from the network:
override func viewDidLoad() {
let quotes = CachedQuotes.getAllQuotes()
//Prints the number of saved records which is 0 now
self.getQuote { (result, error) in
if let qoutes = result?.quotes {
CachedQuotes.add(word: "friend", quotes: qoutes, language: "en")
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let quotes = CachedQuotes.getAllQuotes()
//Prints the number of saved records which is 10 now
}
But when I re-run the app, nothing is saved into the persistance container.
UPDATE:
The code below works now
static func saveContext () {
let mainContext = persistentContainer.viewContext
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.automaticallyMergesChangesFromParent = true
privateContext.parent = mainContext
privateContext.perform {
do {
try privateContext.save()
mainContext.perform({
do {
try mainContext.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
})
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
First it saves the private quoue then saves the main.
let mainContext = persistentContainer.viewContext
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = mainContext
You edit a context and then save the same context to persist the changes. Creating a child context to .viewContext and saving said child context does not save the .viewContext itself, where you made changes.
If you want to use background queues, first set var automaticallyMergesChangesFromParent: Bool on the .viewContext where you want to receive changes from the background queue. Then you create a background context, set on it the same persistentStoreCoordinator from .viewContext, make changes on it and then save the background queue.
Using privateContext.perform is a good start. You can do better if you wrap the changes to quote in a perform through the context in which the quote was created in the first place, so you access quote through the same thread the context uses.
Here is the singleton from Apple's Refreshing and Maintaining Your App Using Background Tasks sample.
import Foundation
import CoreData
class PersistentContainer: NSPersistentContainer {
private static let lastCleanedKey = "lastCleaned"
static let shared: PersistentContainer = {
ValueTransformer.setValueTransformer(ColorTransformer(), forName: NSValueTransformerName(rawValue: String(describing: ColorTransformer.self)))
let container = PersistentContainer(name: "ColorFeed")
container.loadPersistentStores { (desc, error) in
if let error = error {
fatalError("Unresolved error \(error)")
}
print("Successfully loaded persistent store at: \(desc.url?.description ?? "nil")")
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
return container
}()
var lastCleaned: Date? {
get {
return UserDefaults.standard.object(forKey: PersistentContainer.lastCleanedKey) as? Date
}
set {
UserDefaults.standard.set(newValue, forKey: PersistentContainer.lastCleanedKey)
}
}
override func newBackgroundContext() -> NSManagedObjectContext {
let backgroundContext = super.newBackgroundContext()
backgroundContext.automaticallyMergesChangesFromParent = true
backgroundContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
return backgroundContext
}
}
Personally I prefer passing the NSPersistentContainer around via dependency injection but it requires a lot more effort.