Fetch data from CoreData for iOS 14 widget - swift

I want to display data fetched from Core Data in a widget. But #FetchRequest doesn’t work on widgets.
As I understand, we have to create an app group and make a shared persistent container.
What I want to know is how to read (fetch) data on widgets from that shared persistent container or simply, how to display data fetched from Core Data in widgets.

First you need to create an AppGroup which will be used to create a Core Data Persistent Container (here is a good explanation how to do it)
Then you need to create your own CoreData stack (an example can be found when you create a new empty project with CoreData enabled).
Accessing Core Data Stack in MVVM application
Assuming you have already created your Core Data model (here called DataModel), you now need to set the container url to your custom shared container location:
Share data between main App and Widget in SwiftUI for iOS 14
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: <your_app_group>)!
let storeURL = containerURL.appendingPathComponent("DataModel.sqlite")
let description = NSPersistentStoreDescription(url: storeURL)
let container = NSPersistentContainer(name: "DataModel")
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { ... }
Now you can get the managedObjectContext from your shared Persistent Container:
let moc = CoreDataStack.shared.managedObjectContext
and perform a fetch request with it (more information here)
let predicate = NSPredicate(format: "attribute1 == %#", "test")
let request = NSFetchRequest<SomeItem>(entityName: "SomeItem")
let result = try moc.fetch(request)
Apart from all the links above I recommend you also read this tutorial about Core Data:
Core Data with SwiftUI Tutorial: Getting Started
Here is a GitHub repository with different Widget examples including the Core Data Widget.

For people who did all the work above, and finally can get the connection to your Core Data (e.g. you can get the count of request), but can't fetch the request, is mostly because that the entity you're fetching contains transformable type, and for some reason this error occurred: Cannot decode object of class, try fix this.

Related

SwiftUI List memory issue, images not deallocating from RAM causing crash

I am loading images into a SwiftUI List. When too many images are scrolled down, RAM skyrockets and crashes app. Why are images not deallocated as user scrolls down past them?
I am loading images as so:
List(allProducts, id: \.self) { product in
Image(uiImage: UIImage(data: dataFromRealmDB[product]))
}
My intuition tells me that there must be something to deallocate it manually from memory, so I am trying the following. Please let me know if you how to fill in the blank.
List(allProducts, id: \.self) { product in
Image(uiImage: UIImage(data: dataFromRealmDB[product])).onDisappear(perform: {
"WHAT SHOULD GO HERE TO MAKE THE IMAGE GET PURGED FROM RAM?"
}
}
If my suggested solution is not possible, please let me know as well.
UPDATE
I have changed the way images are stored. Now they are stored with FileManager instead of saving them to the RealmDB. This is my function to get the image. Still memory usage increase causing a crash, there is no deallocation from SwiftUI.
func getImage(link: String) -> Image? {
let lastPath = URL(string: link)?.lastPathComponent
if let dir = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) {
let image : UIImage? = UIImage(contentsOfFile: URL(fileURLWithPath: dir.absoluteString).appendingPathComponent(lastPath!).path)
if image != nil {
return Image(uiImage: UIImage(contentsOfFile: URL(fileURLWithPath: dir.absoluteString).appendingPathComponent(lastPath!).path)!)
}
}
return Image("noImg")
}
The code in the question is a bit incomplete but the answer really isn't a coding answer, it's more of a overall design answer.
Realm objects are lazily loaded and will essentially never overrun memory if used properly; they are only in memory when in use; e.g. a Realm Results object having 10,000 objects is easily handled, memory is allocated and deallocated automatically.
On that note, if you store Realm objects in other, non-Realm objects like an array, that totally changes the memory impact and can overwhelm the device.
But, and more importantly:
Realm is not a good way to store Blob data (full size pictures). A Realm property has a finite amount of storage of 16Mb and an image can easily go way beyond that.
There are other options for picture storage from MongoDB and Firebase.
See my answer to this question for more details
Lastly, you should be using pagination to control how many images are loaded at a time from whatever source you use; that will allow you to more easily control the memory allocation and UI.
So that last part is important, no matter what the technique is, loading a bunch of images into memory is going to eventually overwhelm the device so as you can see, switching to using FileManger to load the images from disk instead of Realm is not going to be a long term solution; pagination is the way to go.
There are some third party libraries available to help with that and/or you can craft your own. Do a search here on SO for 'image pagination' as it's discussed a lot
Oh - one other thing; please use thumbnails for your UI! You only really need to display full size images if the user taps or selects it - then you can load it from disk. Firebase Storage can actually do that for you; when you upload an full size image, it can (on their server) create a thumbnail for your UI.
Likewise, thumbnails are tiny and Realm can easily handle those; the design would be an object like this
class MyImageObject: Object {
#Persisted var imageURL: //a string or url to where it's stored on disk
#Persisted var thumbnail: Data!
#Persisted var image_name = "" //the image name
}
Are you sure it is not related to the fact you fetch the image raw data from the Database? According this question a SwiftUI List works like a tableview - i.e reusing cells.
I think the fact you are using a data base to hold the raw data of the images causes the spike in memory.
This is kind of an opinion based answer but I’d recommend on either bundling the images in advance in the application (if your business logic supports it), using names in the DB and init them by name.
Or hosting them remotely and fetching on demand.

SwiftUI + macOS + document based application problems

I am trying to build a document based SwiftUI application with core data enabled. Starting from the template in Xcode (11.5): New project -> macOS + App -> Swift + SwiftUI + "Create document-based application" + "Use Core data". After that I try to add an entity in model editor with just two attributes, "id" and "amount". The model is called "Trans" and codegen is set on "Class Definition".
In the provided Content view I add the code below so I can access the managed object context.
#Environment(\.managedObjectContext) var moc
So far it's working as expected. After that I try to add a simple fetch request.
#FetchRequest(entity: Trans.entity(), sortDescriptors: []) var trans: FetchedResults<Trans>
Then this error comes up in the console.
No NSEntityDescriptions in any model claim the NSManagedObject subclass 'myapp.Trans' so +entity is confused. Have you loaded your NSManagedObjectModel yet?
I am quite new to Core Data so I don't even know where to start looking. Is this broken? Or am I supposed to add more code to get it to work? Can someone please point in the right direction?
I had this exact problem creating an app using Xcode12 (Beta 2) and the multi-platform option.
What works is to divide the code up so that your context is fully initialized before you try to use it.
So take the following line of code and split it into two lines of code:
// Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
// Add `#Environment(\.managedObjectContext)` in the views that will need the context.
let contentView = ContentView().environment(\.managedObjectContext, persistentContainer.viewContext)
Dividing it up into two lines:
// Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
// Add `#Environment(\.managedObjectContext)` in the views that will need the context.
let context = persistentContainer.viewContext
let contentView = ContentView().environment(\.managedObjectContext, context)
It's a small change, but everything compiles and runs correctly now.

How to read an existing core data persistence store, without creating a new one?

I am trying to migrate a Core Data database to Realm. I'm okay with changing the APIs in my code (I actually did a whole rewrite of the app), but the problem is migrating the existing data stored on users' devices.
To read data from Core Data, I first create a NSManagedObjectContext, and set its persistentStoreCoordinator to a coordinator:
// app delegate, didFinishLaunching
var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
To create this coordinator, I do:
let modelURL = Bundle.main.url(forResource: "MyModel", withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL)!
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
Then at this point, I don't know what to do next. Normally in a Core Data app, I would add a persistent store at a desired URL...
let url = aDesiredURL.appendingPathComponent("MyPersistentStore.sqlite")
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: [NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true])
...just like the Core Data boilerplate that Xcode generates for you when you check "Use Core Data" when creating a project.
However, since I'm migrating, I don't want to add a new persistent store. If there is already a "MyPersistentStore.sqlite" file stored at aDesiredURL, I just want to read from that.
From my experimentation, addPersistentStore seems to create a new MyPersistentStore.sqlite file if it doesn't exist, and read from it if it already exists, but I want a method to read from MyPersistentStore.sqlite if it exists, and not create a new file (maybe throw an error?) if it doesn't.
I looked through the methods of NSPersistentStoreCoordinator in its documentation, and found persistentStore(for:) which sounded promising. However, it returns nil whether or not the .sqlite file exist.
I also tried not doing anything after creating the coordinator, but that didn't work either: I wasn't able to retrieve any managed objects from the context (the fetch request returned 0 objects).
Code that fetches the entities for reference:
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "MyEntity")
let managedObjects = try managedObjectContext.fetch(fetchRequest)
print(managedObjects.count)
How can I only read from the .sqlite file representing the Core Data persistent store, without creating a new one?

App Hangs when saving in core data

I'm using swift core data to save user's info within the app but when its saving current users data (name,bio, 3-4 photos) it hangs for 2-3 seconds until it saves the data. What is the best way to avoid this hanging of the app? this is the code I'm using
for photo in photos {
let data = NSData(contentsOfURL: NSURL(string: photo.url!)!)
let newimg = UIImage(data: data!)
newusersImages.addObject(newimg!)
}
mayBeUser.name = currentuser!.objectForKey("name") as! String
let arrayData = NSKeyedArchiver.archivedDataWithRootObject(newusersImages)
let locationData = NSKeyedArchiver.archivedDataWithRootObject(currentuser!.objectForKey("location")!)
mayBeUser.photos = arrayData
mayBeUser.location = locationData
mayBeUser.about = currentuser!.objectForKey("about") as! String
mayBeUser.setValue(currentuser!.objectForKey("age") as! Int, forKey: "age")
mayBeUser.objectId = currentuser!.objectId!
mayBeUser.lastSeen = currentuser!.objectForKey("lastSeen") as! NSDate
try! context.save()
print("Current User Updated")
when the user has 10-15 friends , it takes hangs for a minute to save/update all the info/data of the users.
Here's how I'd do it. Now obviously you don't want the UI to hang, so you'll want to use a managed object context that has a private queue concurrency type. More info on that here. Specifically, look at the part at the end where he details how to create the export function.
Now, since you're saving data to the persistent store in this case, you might not want the user to go around changing stuff while their previous changes are being saved, in this case you might want an interim screen showing the progress of the save.
You (and your app) are having to work hard to save all that photo data into one attribute of your User entity. Instead, restructure your data model:
Create a separate Photo entity for the photos.
Don't store the photo image data in CoreData - just store a string with the URL.
Add a to-many relationship from User to Photo, instead of the photos attribute.
Incidentally, I would avoid using objectId as an attribute name: it's so close to CoreData's objectID it will cause confusion.

Text and table view data storage in swift

Could anyone tell me what the way of storing a long text in a swift app is. Let's suppose I have an app that has a table view and when I chose a row I go to a new scene where I have a big page filled by text.
The question is where do I have to store the data of the table's row and the whole text? And how?
Do I have to make a model? Is it just one for both the table and the text? Or more than one. Is there any tutorial that explains this exact situation or close to it?
You could use Parse.com framework to retrieve data from a database. There is a lot of documentation on this.
In this link you can find a tutorial that will explain you how to load data from Parse and show it in your UITableView.
Storing Local (in-memory store):
For storing without a internet connection you could use Core Data Stack with NSInMemoryStoreType as storeType. This tutorial will give you a nice idea on how it works.
You can declare a model like it follows:
struct CoreDataModel {
let name: String
let bundle: NSBundle
init(name: String, bundle: NSBundle)
// other properties & methods
}
And then manage it with:
let model = CoreDataModel(name: "MyModel", bundle: myBundle)
let stack = CoreDataStack(model: model,
storeType: NSInMemoryStoreType,
concurrencyType: .MainQueueConcurrencyType)
// Use context
stack.managedObjectContext