Swift - Pass the CoreDataStack or just Context? - swift

I'm trying to figure out Core Data. I've been following some different tutorials and they all do things a bit differently.
I have a CoreDataStack and it's initialized in SceneDelegate
lazy var coreDataStack = CoreDataStack(modelName: "model")
I believe I then use dependency injection? to set a corresponding property in the viewControllers
guard let tabController = window?.rootViewController as? UITabBarController,
let viewController = navigationController.topViewController as? ViewController else {
fatalError("Application storyboard mis-configuration. Application is mis-configured")
}
viewController.coreDataStack = coreDataStack
viewController.context = coreDataStack.ManagedObjectContext
My questions is should I pass the entire coreDataStack object to the next view? Or just the context?
Initially I was passing the entire coreDataStack, Everything seemed to work just fine. But I wasn't sure if that was correct since most tutorials seem to only reference the context. (But even then, most tutorials are vastly different, even when they are made by the same author.)
import UIKit
import CoreData
class CoreDataStack {
private let modelName: String
init(modelName: String) {
self.modelName = modelName
setupNotificationHandling()
}
lazy var managedContext: NSManagedObjectContext = {
return self.storeContainer.viewContext
}()
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
}()
// MARK: - Notification Handling
func saveForDidEnterBackground() {
saveContext()
}
#objc func saveChanges(_ notification: Notification) {
saveContext()
}
// MARK: - Helper Methods
private func setupNotificationHandling() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(saveChanges(_:)),
name: UIApplication.willTerminateNotification,
object: nil)
}
// MARK: -
private func saveContext() {
guard managedContext.hasChanges else { return }
do {
try managedContext.save()
} catch {
print("Unable to Save Managed Object Context")
print("\(error), \(error.localizedDescription)")
}
}
}

Related

Core Data with SwiftUI MVVM Feedback

I am looking for a way to use CoreData Objects using MVVM (ditching #FetchRequest). After experimenting, I have arrived at the following implementation:
Package URL: https://github.com/TimmysApp/DataStruct
Datable.swift:
protocol Datable {
associatedtype Object: NSManagedObject
//MARK: - Mapping
static func map(from object: Object) -> Self
func map(from object: Object) -> Self
//MARK: - Entity
var object: Object {get}
//MARK: - Fetching
static var modelData: ModelData<Self> {get}
//MARK: - Writing
func save()
}
extension Datable {
static var modelData: ModelData<Self> {
return ModelData()
}
func map(from object: Object) -> Self {
return Self.map(from: object)
}
func save() {
_ = object
let viewContext = PersistenceController.shared.container.viewContext
do {
try viewContext.save()
}catch {
print(String(describing: error))
}
}
}
extension Array {
func model<T: Datable>() -> [T] {
return self.map({T.map(from: $0 as! T.Object)})
}
}
ModelData.swift:
class ModelData<T: Datable>: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
var publishedData = CurrentValueSubject<[T], Never>([])
private let fetchController: NSFetchedResultsController<NSFetchRequestResult>
override init() {
let fetchRequest = T.Object.fetchRequest()
fetchRequest.sortDescriptors = []
fetchController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: PersistenceController.shared.container.viewContext, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchController.delegate = self
do {
try fetchController.performFetch()
publishedData.value = (fetchController.fetchedObjects as? [T.Object] ?? []).model()
}catch {
print(String(describing: error))
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let data = controller.fetchedObjects as? [T.Object] else {return}
self.publishedData.value = data.model()
}
}
Attempt.swift:
struct Attempt: Identifiable, Hashable {
var id: UUID?
var password: String
var timestamp: Date
var image: Data?
}
//MARK: - Datable
extension Attempt: Datable {
var object: AttemptData {
let viewContext = PersistenceController.shared.container.viewContext
let newAttemptData = AttemptData(context: viewContext)
newAttemptData.password = password
newAttemptData.timestamp = timestamp
newAttemptData.image = image
return newAttemptData
}
static func map(from object: AttemptData) -> Attempt {
return Attempt(id: object.aid ?? UUID(), password: object.password ?? "", timestamp: object.timestamp ?? Date(), image: object.image)
}
}
ViewModel.swift:
class HomeViewModel: BaseViewModel {
#Published var attempts = [Attempt]()
required init() {
super.init()
Attempt.modelData.publishedData.eraseToAnyPublisher()
.sink { [weak self] attempts in
self?.attempts = attempts
}.store(in: &cancellables)
}
}
So far this is working like a charm, however I wanted to check if this is the best way to do it, and improve it if possible. Please note that I have been using #FetchRequest with SwiftUI for over a year now and decided to move to MVVM since I am using it in all my Storyboard projects.
For a cutting edge way to wrap the NSFetchedResultsController in SwiftUI compatible code you might want to take a look at AsyncStream.
However, #FetchRequest currently is implemented as a DynamicProperty so if you did that too it would allow access the managed object context from the #Environment in the update func which is called on the DynamicProperty before body is called on the View. You can use an #StateObject internally as the FRC delegate.
Be careful with MVVM because it uses objects where as SwiftUI is designed to work with value types to eliminate the kinds of consistency bugs you can get with objects. See the doc Choosing Between Structures and Classes. If you build an MVVM object layer on top of SwiftUI you risk reintroducing those bugs. You're better off using the View data struct as it's designed and leave MVVM for when coding legacy view controllers. But to be perfectly honest, if you learn the child view controller pattern and understand the responder chain then there is really no need for MVVM view model objects at all.
And FYI, when using Combine's ObservableObject we don't sink the pipeline or use cancellables. Instead, assign the output of the pipeline to an #Published. However, if you aren't using CombineLatest, then perhaps reconsider if you should really be using Combine at all.

How do I refresh my SwiftUI view when my core data store has been updated from a background extension?

I have a SwiftUI app with an intents extension target. I also have a core data store located on a shared app group.
The intents extension can successfully write data to the store using newBackgroundContext() in the Shortcuts app. My SwiftUI app can read the data from the app group in viewContext but only when I force quit the app and re-open it.
I think I need a way to let my app know that it has been updated in a different context but I'm not sure how do do that?
I've tried setting context.automaticallyMergesChangesFromParent = true in the app delegate and adding context.refreshAllObjects() to onAppear in the SwiftUI view but that doesn't seem to make any difference.
mergeChanges(fromContextDidSave:) seems promising but I'm not sure how to get the Notification.
I'm new to Core Data and very confused, so any pointers in the right direction would be great, thanks!
Here's a sanitised version of my current code:
App Delegate
lazy var persistentContainer: NSCustomPersistentContainer = {
let container = NSCustomPersistentContainer(name: "exampleContainerName")
container.loadPersistentStores { description, error in
if let error = error {
print("error loading persistent core data store")
}
}
return container
}()
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
print("error saving core data context")
}
}
}
class NSCustomPersistentContainer: NSPersistentContainer {
override open class func defaultDirectoryURL() -> URL {
var storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "exampleAppGroupName")
storeURL = storeURL?.appendingPathComponent("exampleContainerName")
return storeURL!
}
}
Scene Delegate
guard let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext else {
fatalError("Unable to read managed object context.")
}
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ExampleView.environment(\.managedObjectContext, context))
self.window = window
window.makeKeyAndVisible()
}
Swift UI View
import SwiftUI
import CoreData
struct ExampleView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: ExampleEntityName.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \ExampleEntityName.date, ascending: false),
]
) var items: FetchedResults<ExampleEntityName>
var body: some View {
VStack {
List(items, id: \.self) { item in
Text("\(item.name)")
}
}
}
}
Intent Extension
class NSCustomPersistentContainer: NSPersistentContainer {
override open class func defaultDirectoryURL() -> URL {
var storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "exampleContainerName")
storeURL = storeURL?.appendingPathComponent("exampleAppGroupName")
return storeURL!
}
}
let persistentContainer: NSCustomPersistentContainer = {
let container = NSCustomPersistentContainer(name: "exampleContainerName")
container.loadPersistentStores { description, error in
if let error = error {
print("error loading persistent core data store: \(error)")
}
}
return container
}()
let context = persistentContainer.newBackgroundContext()
let entity = NSEntityDescription.entity(forEntityName: "ExampleEntityName", in: context)
let newEntry = NSManagedObject(entity: entity!, insertInto: context)
newEntry.setValue(Date(), forKey: "date")
newEntry.setValue("Hello World", forKey: "name")
do {
try context.save()
} catch {
print("Failed saving")
}
Don't know if you discovered an answer yet, but I had a similar problem using UserDefaults and an intent extension in a shared app group. The thing that finally worked for me was adding a call to load the items in the method below in the SceneDelegate:
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
SiriResponseManager.shared.grabUD()
}
In my example I am using a singleton that houses the method to load the items from the UserDefaults. This singleton class is an ObservableObject. In my first view that loads (ContentView in my case) I assign an ObservedObject variable to my singleton class and retrieve the #State variables in my singleton to populate the string in my view.
Hopefully this helps.
class Data: ObservableObject {
#Published var dataToUpdate: Int
}
struct UpdateView : View {
#EnvironmentObject data: Data
...body ... {
// use data.dataToUpdate here
}
}
Just define data like you see in this example. Then fill dataToUpdate whenever your database changes...of course this is just a simplified example with just an Int Value...

Swift completion handler in class and function

I have a Class with a function that connect to a firestoreDB and get some data:
import UIKit
import CoreLocation
import Firebase
private let _singletonInstance = GetBottlesFromDB()
class GetBottlesFromDB: NSObject {
class var sharedInstance: GetBottlesFromDB { return _singletonInstance }
var Pins = [LayoutBottlesFromDB]()
// MARK: - init
override init() {
super.init()
populatePinList(completion: { pin in self.Pins } )
//print("GET ALL PINS: \(Pins)")
}
func populatePinList(completion: #escaping ([LayoutBottlesFromDB]) -> ()) {
Pins = []
AppDelegate.ADglobalVar.db.collection("Bottles").whereField("pickupuser", isEqualTo: NSNull()).getDocuments { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
print("start getting documents:")
for document in querySnapshot!.documents {
//print("\(document.documentID) => \(document.data())")
//print("\(document.documentID)")
let bottleID:String = document.documentID
let bottlekind:Int = document.data()["bottle"] as! Int
var bottletitel:String
var bottlesub:String
var bottleurl:String = (document.data()["pic"] as? String)!
let pin = LayoutBottlesFromDB(document.data()["lat"] as! CLLocationDegrees, document.data()["long"] as! CLLocationDegrees, ID: bottleID, title: bottletitel, subtitle: bottlesub, type: bottlekind, url:bottleurl)
//print("GET DAATA from DB: \(pin)")
self.Pins.append(pin)
} //for
completion(self.Pins)
} //else
} //querysnap
}//function
}//class
in my ViewController I call this function.
for pin in GetBottlesFromDB.sharedInstance.Pins{
print("Add Pin : \(pin)")
}
My pProblem is that the function will called but the print is empty.
The function doesn't wait for a completion. What did I do wrong?
You are calling directly GetBottlesFromDB.sharedInstance.Pins and this will not wait for completion of populatePinList method so that's why you are getting blank So You need to wait for completion or you can check if data is not available in pins variable the you need to call completion method like this way:
GetBottlesFromDB.sharedInstance.populatePinList { (pins) in
for pin in pins{
print("Add Pin : \(pin)")
}
}
Nothing in your code waits for the execution of the asynchronous method, so that's no surprise. Also, it would be a terrible design because it would block your app. In addition, your singleton implementaion is overly verbose and doesn't guarantee that it stays a singleton, so I'd recommend to change it to
class GetBottlesFromDB {
private(set) var Pins = [LayoutBottlesFromDB]()
static let shared = GetBottlesFromDB()
private init() {}
// populatePinList as before
}
and in your view controller, e.g. in viewDidLoad do:
override func viewDidLoad() {
GetBottlesFromDB.shared.populatePinList { pins in
pins.forEach { print("Add Pin: \(pin)") }
}
}

How to remove weak reference from object? in Swift

I have a class which is named ChatDataSource. The ChatDataSource has a ChatManager class like lazy var. If a user will not do any action ChatManager will not to init. ChatManager has some blocks which are calling some functions from ChatDataSource. I made in closers weak references for ChatDataSource. I noticed that ChatDataSource object never deinit from memory because the ChatManager keeps him weak link. The problem also lies in the fact that when I open a new conversation and init a new ChatDataSource, the old ChatDataSource is in memory. ChatManager also lives in other controllers. How can I remove a weak refence from ChatManager?
A snippet of my code
class ChatDataSource: ChatDataSourceProtocol {
fileprivate lazy var chatManager: ChatManager = {
let chatManager = ChatManager()
chatManager.onMessageChanged = { [weak self] (message) in
guard let sSelf = self else { return }
sSelf.delegate?.chatDataSourceDidUpdate(sSelf)
}
chatManager.changeUIMessageByID = { [weak self] (id) in
guard let sSelf = self else { return }
guard let uiID = sSelf.messageDataDictionary[id] else { return }
let message = sSelf.messages[uiID]
message.status = .success
sSelf.delegate?.chatDataSourceDidUpdate(sSelf)
}
return chatManager
}()
}
I tried to make like. But it doesn't work.
chatManager.didLeaveConversation = { [weak self] in
self = nil
}

Use Core Data to Insert data asynchronously in Swift with UIProgressView

I receive a JSon that I convert to a array of dictionaries and insert this array to Core Data.
The result of this Array has 8.000 items.
How to insert an UIProgressView while the Core Data is working with the code bellow?
Thank you!
func CreateContext() -> NSManagedObjectContext {
let AppDel: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let Context: NSManagedObjectContext = AppDel.managedObjectContext
return Context
}
static func AddData(arrayDictionary: [[String : AnyObject]]) {
for Item in arrayDictionary {
let Context = CreateContext()
let DAO = NSEntityDescription.insertNewObjectForEntityForName("Customers", inManagedObjectContext: Context)
// Insert at Core Data
if let item = Item["UserID"] as? Int {
DAO.setValue(item, forKey: "userID")
}
if let item = Item["Name"] as? String {
DAO.setValue(item, forKey: "name")
}
if let item = Item["Email"] as? String {
DAO.setValue(item, forKey: "email")
}
do {
try Contexto.save()
} catch {
let nserror = error as NSError
//abort()
}
}
}
}
EDIT
The data is being inserted by a custom class. I tried to create a Protocol and Delegate but I don't know where is the error (sincerely, I don't know how to work with Protocol and Delegate. I tried to follow the example in Howto Update a gui (progressview) from a download delegate in swift)
My ViewController class:
import UIKit
protocol ProgressBarDelegate: class {
func UpdateProgressBar(progress: Float)
}
class MyViewController: UIViewController {
#IBOutlet var progressView: UIProgressView!
func AddDataFromArray() {
let DB = MyCustomClass()
DB.delegate?.UpdateProgressBar(progressView.progress)
DB.AddData(getArrayDictionaryData())
}
}
My Custom class:
class MyCustomClass {
var delegate: ProgressBarDelegate?
func initWithDelegate(delegate: ProgressBarDelegate) {
self.delegate = delegate
}
func CreateContext() -> NSManagedObjectContext {
let AppDel: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let Context: NSManagedObjectContext = AppDel.managedObjectContext
return Context
}
func AddData(arrayDictionary: [[String : AnyObject]]) {
var addedItems: Int = 0
var Progress = Float()
for Item in arrayDictionary {
addedItems += 1
Progress = ((Float(100.0) / Float(arrayDictionary.count)) * Float(addedItems)) / Float(100)
let Context = CreateContext()
let DAO = NSEntityDescription.insertNewObjectForEntityForName("Customers", inManagedObjectContext: Context)
// Insert at Core Data
if let item = Item["UserID"] as? Int {
DAO.setValue(item, forKey: "userID")
}
if let item = Item["Name"] as? String {
DAO.setValue(item, forKey: "name")
}
if let item = Item["Email"] as? String {
DAO.setValue(item, forKey: "email")
}
do {
try Contexto.save()
} catch {
let nserror = error as NSError
//abort()
}
delegate!.UpdateProgressBar(Progress)
}
}
}
You seem to be misunderstanding the relationships a little bit.
A class can take a delegate if it requires some information to be supplied or wants to make a callback to let someone know that something happened. The delegate implements the methods specified in the delegate protocol and handles the notifications:
Delegate (observer)
-> conforms to the delegate protocol
-> registers itself
-> handles notifications
Interesting class
-> holds a reference to a delegate
-> notifies the delegate when things happen
In your case. MyViewController should conform to the delegate protocol, so it should implement UpdateProgressBar. It creates an instance of MyCustomClass to do some work for it and sets itself as the delegate. MyCustomClass then does some work and calls UpdateProgressBar, where MyViewController then updates the UI.
You get a crash at the moment because you never set the delegate. This line:
DB.delegate?.UpdateProgressBar(progressView.progress)
should be
DB.delegate = self
aside:
Don't call something CreateContext if it isn't actually creating something. Also, the first letter of functions should be lower case.
Assuming that the progress view already exists, you'd do something like:
self.progressView.progress = 0
for (index, item) in arrayDictionary.enumerate() {
// Do all the work of importing the data...
let progress = CGFloat(index) / CGFloat(arrayDictionary.count)
self.progressView.progress = progress
}
The value of progress will gradually work its way up from nearly zero to 1.0 as the loop keeps executing.