SwiftUI crashes due to deleted CoreData object - core-data

I have a SwiftUI application, that updates views based on fetched CoreData objects, like so:
var body: some View {
HStack {
VStack(alignment:.leading) {
TaskProgressBar(model: ProgressModel(frequency: Int(task.frequency), deadline: task.deadline))
}
}
}
The app crashes because task.frequency becomes unexpectedly nil, when I delete the CoreData object:
func deleteTasks(_ tasks: [Task]) {
for task in tasks {
persistenContainer.viewContext.delete(task)
}
do {
try persistenContainer.viewContext.save()
} catch {
fatalError("Failed to save context after task deletion")
}
}
I was able to fix it by calling save() asynchronously, but I am just wondering if that is the right solution? Is there a race condition and could my app still potentially crash, if saving asynchronously?

Related

Extended fe_user model, can't access crdate when run via cronjob

I have a problem with accessing the crdate of a fe_user in a croniob task. I have an extended Frontend user model where I added this adjustments:
https://stackoverflow.com/a/50663006/1684975
Additionaly I've added a mapping via ts
config.tx_extbase {
persistence {
classes {
TYPO3\CMS\Extbase\Domain\Model\FrontendUser {
subclasses {
Tx_MyExt_User = ACV\MyExt\Domain\Model\User
}
}
Paul\MyExt\Domain\Model\User {
mapping {
tableName = fe_users
recordType = Tx_MyExt_User
columns {
crdate.mapOnProperty = crdate
}
}
}
}
}
}
When I fire up the Scheduler task manual via the Scheduler BE module it's ok. But when the real cronjob kicks in I get the error
Uncaught TYPO3 Exception Call to a member function getTimestamp() on null
In the Scheduler task, I get the crDate via the getter and try to get the timestamp…
$users = $frontendRepository->findByOptIn(false);
foreach ($users as $user) {
if ($user->getCrdate()->getTimestamp() < strtotime('-5 days')) {
$frontendRepository->remove($user);
}
}
The mode of the user is correct. I can access other custom properties I've added to the model.
It's TYPO3 v9.5.26.
The odd thing is that it runs locally in my dev environment.
Did someone have an idea what could cause the problem?
Add a file <extensionfolder>/ext_typoscript_setup.typoscript and add your TypoScript in there:
config.tx_extbase {
persistence {
classes {
TYPO3\CMS\Extbase\Domain\Model\FrontendUser {
subclasses {
Tx_MyExt_User = ACV\MyExt\Domain\Model\User
}
}
Paul\MyExt\Domain\Model\User {
mapping {
tableName = fe_users
recordType = Tx_MyExt_User
columns {
crdate.mapOnProperty = crdate
}
}
}
}
}
}
Alternativly you can add an include there to your TypoScript file in your extension Configuration/TypoScript folder.
If you are in CLI Context, it can be that the TypoScript is not loaded fully for the mapping/extbase configuration, which would match your description that in the BE Context it works, but not in Cronjob/CLI Context.

How to create a NSManagedObject in a modal view using SwiftUI?

How does one create a new ManagedObject (MO) in a modal view using SwiftUI?
Running into a strange bug where Xcode consumes GBs of memory and fills up the hard drive on the mac through swap files.
When one creates the Modal view in the .sheet modifier there appears to be some kind of infinite loop created that fills the memory with duplicates of the ManagedObject that is injected into that Modal view.
This sample project illustrates at least part of the issue. If you run it you see the method called in the .sheet modifier fires over and over again. A theory is that the screen underneath that displays a list of ManagedObjects is causing some kind of loop between the two screens.
https://github.com/sphericalwave/ChildContextTest
Was hoping to use a childContext in the modal screen so any unsaved changes would be discarded if the modal view was dismissed without saving the childContext. But need to clear this hurdle first and there is some challenge involved in sharing ManagedObject across contexts.
import CoreData
import SwiftUI
struct CrtFdsUI: View
{
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: CrtFd.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \CrtFd.scale, ascending: true)])
var crtFds: FetchedResults<CrtFd>
#State var showModal = false
#ObservedObject var absFd: AbsFd
func crtFdModal() -> CrtFdUI {
print("func crtFdModal() -> CrtFdUI")
let cF = CrtFd(scale: 1.0, absFd: absFd, moc: moc)
return CrtFdUI(crtFd: cF)
}
var body: some View {
NavigationView {
VStack {
List {
ForEach(self.crtFds, id: \.objectID) {
CrtFdCell(crtFd: $0)
}
}
.navigationBarTitle("CrtFdsUI")
.navigationBarItems(trailing: PlusBtn(showModal: $showModal))
}
.sheet(isPresented: $showModal) { self.crtFdModal() } //FIXME: Called in endless loop?
}
}
}
Here's the list of managedObjects.
import CoreData
import SwiftUI
struct CrtFdsUI: View
{
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: CrtFd.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \CrtFd.scale, ascending: true)])
var crtFds: FetchedResults<CrtFd>
#State var showModal = false
#ObservedObject var absFd: AbsFd
func crtFdModal() -> CrtFdUI {
print("func crtFdModal() -> CrtFdUI")
let cF = CrtFd(scale: 1.0, absFd: absFd, moc: moc)
return CrtFdUI(crtFd: cF)
}
var body: some View {
NavigationView {
VStack {
List {
ForEach(self.crtFds, id: \.objectID) {
CrtFdCell(crtFd: $0)
}
}
.navigationBarTitle("CrtFdsUI")
.navigationBarItems(trailing: PlusBtn(showModal: $showModal))
}
.sheet(isPresented: $showModal) { self.crtFdModal() }
}
}
}
Right now the code creates a new instance of CrtFd every time the view hierarchy is recalculated. That's probably not a good idea since the hierarchy might be recalculated unexpectedly for reasons you don't directly control, so that even without an infinite loop of creation you'd still probably end up with more new managed objects than you want.
I downloaded your project and I'm not sure what the two Core Data entities represent, but I did notice that when CrtFdsUI first appears, its absFd already has a value for the crtFd property, and that the crtFd property is to-one rather than to-many. That means that when you create a new instance in this code, you're replacing one CrtFd with another that's an exact duplicate.
I'm going to guess that you don't really want to replace one instance with an identical duplicate, so one way of avoiding the problem, then, is to change your crtFdModal() to use the one that already exists, and only create a new copy if there isn't one already:
func crtFdModal() -> CrtFdUI {
print("func crtFdModal() -> CrtFdUI \(showModal)")
let cF = absFd.crtFd ?? CrtFd(scale: 1.0, absFd: absFd, moc: moc)
return CrtFdUI(crtFd: cF)
}
That avoids the problem you describe. It's hard to tell if it's exactly what you need, but since your code creates apparently-unnecessary duplicates it seems likely that it is.

SwiftUI and FetchRequest : delay the reordering of a list

I have written an application displaying a list of students. I use a List of NavigationLink for that purpose. The students are ordered by one of their properties questionAskedClass which is an integer. All this information is stored within CoreData.
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Student.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Student.questionAskedClass,
ascending: true)])
var students: FetchedResults<Student>
var body: some View {
NavigationView {
List {
ForEach(students) {student in
NavigationLink(destination: StudentView(student: student)) {
HStack {
VStack(alignment: .leading) {
Text("\(student.firstName)").font(.headline).foregroundColor(Color.black)
Text("\(student.lastName)").font(.headline).foregroundColor(Color.gray)
}
}
}
}
}
}
}
When I press the name of the students, I switch to a new view called StudentView where I can get more information about the student and where I can update the property questionAskedClass
struct StudentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
func askQuestion() {
self.managedObjectContext.performAndWait {
self.student.questionAskedClass += 1
try? self.managedObjectContext.save()
}
}
}
Unfortunately, when I change that property, the ordering of the initial list is changed and I am taken away from the StudentView. The framework seems to get the feeling that the list needs to be reordered. But I just want this list to be reordered when I go back to the list. Not immediately when I change the value of questionAskedClass.
What can I do to mitigate this problem?
Thanks for your help.
You can try creating a simple NSFetchRequest<Student> and use the result of this fetch to update your students list.
#State var students: [Student] = []
fun refresh() {
let fetchRequest = NSFetchRequest<Student>(entityName: "Student")
students = try? managedObjectContext.fetch(fetchRequest) ?? []
}
You can trigger this refresh in onAppear so the list will be updated every time the View appears:
NavigationView {
...
}.onAppear {
self.refresh()
}
Alternatively you can save your context in onAppear of the main view instead of StudentView:
struct StudentView: View {
func askQuestion() {
self.student.questionAskedClass += 1
}
}
NavigationView {
...
}.onAppear {
try? self.managedObjectContext.save()
}
If you want your data to persist when the app is terminated, add this function in AppDelegate (assuming your persistentContainer is declared there:
func applicationWillTerminate(_ application: UIApplication) {
try? persistentContainer.viewContext.save()
}

SwiftUI #FetchRequest after screen lock not fetching data and returning nil properties

I have usual SwiftUI view like this
struct MyView: View {
#FetchRequest var users: FetchedResults<User>
init() {
self._users = FetchRequest(
entity: User.entity(),
sortDescriptors: [
],
predicate: NSPredicate(format: "company.id == %#", companyId)
)
}
var body: some View {
List {
ForEach(Array(self.users.enumerated()), id: \.1.objectID) { (i, user) in
Text("\(user.name)")
}
}
}
But after locking screen/using home button and returning to app. Initially this view wake ups with empty NSManagedObjects, objects seems to be available there is correct users.count value, each object has its appropriate objectID. But other managed object properties are nil. Then sometimes I experience that in subsequent view refreshes it can (I think "fault" this object properties) fetch this properties and displays ok, or can stay with nil values and I have empty results on list or crash depending user.name is forced unwrap or not
user.name! or user.name ?? ""

Managing threads in Grails Services

So I have a service set up to import a large amount of data from a file the user uploads. I want to the user to be able to continue working on the site while the file is being processed. I accomplished this by creating a thread.
Thread.start {
//work done here
}
Now the problem arises that I do not want to have multiple threads running simultaneously. Here is what I tried:
class SomeService {
Thread thread = new Thread()
def serviceMethod() {
if (!thread?.isAlive()) {
thread.start {
//Do work here
}
}
}
}
However, this doesn't work. thread.isAlive() always return false. Any ideas on how I can accomplish this?
I would consider using an Executor instead.
import java.util.concurrent.*
import javax.annotation.*
class SomeService {
ExecutorService executor = Executors.newSingleThreadExecutor()
def serviceMethod() {
executor.execute {
//Do work here
}
}
#PreDestroy
void shutdown() {
executor.shutdownNow()
}
}
Using a newSingleThreadExecutor will ensure that tasks execute one after the other. If there's a background task already running then the next task will be queued up and will start when the running task has finished (serviceMethod itself will still return immediately).
You may wish to consider the executor plugin if your "do work here" involves GORM database access, as that plugin will set up the appropriate persistence context (e.g. Hibernate session) for your background tasks.
Another way to do this is to use Spring's #Async annotation.
Add the following to resources.groovy:
beans = {
xmlns task:"http://www.springframework.org/schema/task"
task.'annotation-driven'('proxy-target-class':true, 'mode':'proxy')
}
Any service method you now annotate with #Async will run asynchronously, e.g.
#Async
def reallyLongRunningProcess() {
//do some stuff that takes ages
}
If you only want one thread to run the import at a time, you could do something like this -
class MyService {
boolean longProcessRunning = false
#Async
def reallyLongRunningProcess() {
if (longProcessRunning) return
try {
longProcessRunning = true
//do some stuff that takes ages
} finally {
longProcessRunning = false
}
}
}

Resources