I've got my own Core Data function, which fetches data. This func is identical to my previous one, except entity names. Previous works great, but this fails with error EXC_BAD_ACCESS when I try to get data from fetchedData .
func fetchGroups() -> Array<Group> {
var groups: Array<Group> = []
let fetchRequest: NSFetchRequest<Public> = Public.fetchRequest()
fetchRequest.sortDescriptors = [SortDescriptor.init(key: "publicTitle", ascending: true)]
let fetchedData = try! context.fetch(fetchRequest)
if (!fetchedData.isEmpty) {
for i in 0...fetchedData.count-1 {
print(fetchedData[0])
var group: Group = Group()
group.groupName = fetchedData[i].publicTitle
group.groupPhoto = fetchedData[i].publicPhoto
group.groupID = Int(fetchedData[i].publicID)
groups.append(group)
}
return groups
}
else {
return groups
}
}
So if it executes the code in brackets after if (!fetchedData.isEmpty), array isn't empty. Why it fails on getting elements?
P.S. fetchedData.count = 1; But fetchedData[0] = BIG CRASH! Magic.
After a long research I found that the problem was too little but very hard to find. I just checked my coredata.xcdatamodeld file and noticed, that Public entity doesn't have Class definition. By default all entities have only names.
Related
I have a SwiftUI app that uses the MVVM design pattern in places where the underlying logic driving the View is either verbose or unit testing is advisable. In certain places I have taken to using a NSFetchedResultsController in conjunction with #Published properties and, early in development, this behaved as I would expect.
However, I have now encountered a situation where an addition to the CoreData store triggers controllerDidChangeContent and the array populated by controller.fetchedObjects has an appropriate number of elements but, for reasons I cannot fathom, I am unable to access the newest elements.
There is a certain amount of data processing which, as I'm working with an array by this point, I didn't think would cause a problem. I'm more suspicious that relationships may be responsible in some way and/or faulting is responsible (although adjusting faulting behaviour on the underlying fetch request failed to resolve the issue).
Interestingly, some similar code elsewhere in the app that uses #FetchRequest (because the View is simpler and so a ViewModel wasn't considered necessary) doesn't seem to suffer from the same problem.
Normally scattering debugging around has put me back on track but not today! I've included the console output - as you can see, as new entries (timestamped) are added, the total observation count increases but the most property which should reflect the most recent observation does not change. Any pointers would be gratefully received as always.
I can't really prune the code on this without losing context - apologies in advance for the verbosity ;-)
ViewModel:
extension ParameterGridView {
final class ViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
#Published var parameters: [Parameter] = []
#Published var lastObservation: [Parameter : Double] = [:]
#Published var recentObservation: [Parameter : Double] = [:]
let patient: Patient
private let dataController: DataController
private let viewContext: NSManagedObjectContext
private let frc: NSFetchedResultsController<Observation>
var observations: [Observation] = []
init(patient: Patient, dataController: DataController) {
self.patient = patient
self.dataController = dataController
self.viewContext = dataController.container.viewContext
let parameterFetch = Parameter.fetchAll
self.parameters = try! dataController.container.viewContext.fetch(parameterFetch)
let observationFetch = Observation.fetchAllDateSorted(for: patient)
self.frc = NSFetchedResultsController(
fetchRequest: observationFetch,
managedObjectContext: dataController.container.viewContext,
sectionNameKeyPath: nil,
cacheName: nil)
try! self.frc.performFetch()
observations = self.frc.fetchedObjects ?? []
super.init()
frc.delegate = self
updateHistoricalObservations()
}
// MARK: - METHODS
/// UI controls for entering new Observations default to the last value entered
/// This function calculates the median value for the Parameter's reference range to be used in the event no historical observations are available
/// - Parameter parameter: Parameter used to derive start value
/// - Returns: median value for the Parameter's reference range
func medianReferenceRangeFor(_ parameter: Parameter) -> Double {
let rangeMagnitude = parameter.referenceRange.upperBound - parameter.referenceRange.lowerBound
return parameter.referenceRange.lowerBound + (rangeMagnitude / 2)
}
/// Adds a new Observation to the Core Data store
/// - Parameters:
/// - parameter: Parameter for the observation
/// - value: Observation value
func addObservationFor(_ parameter: Parameter, with value: Double) {
_ = Observation.create(in: viewContext,
patient: patient,
parameter: parameter,
numericValue: value)
try! viewContext.save()
}
/// Obtains clinically relevant historical observations from the dataset for each Parameter
/// lastObservation = an observation within the last 15 minutes
/// recentObservation= an observation obtained within the last 4 hours
/// There may be better names for these!
private func updateHistoricalObservations() {
let lastObservationTimeLimit = Date.now.offset(.minute, value: -15)!.offset(.second, value: -1)!
let recentObservationTimeLimit = Date.now.offset(.hour, value: -4)!.offset(.second, value: -1)!
Logger.coreData.debug("New Observations.count = \(self.observations.count)")
let sortedObs = observations.sorted(by: { $0.timestamp < $1.timestamp })
let newestObs = sortedObs.first!
let oldestObs = sortedObs.last!
Logger.coreData.debug("Newest obs: \(newestObs.timestamp) || \(newestObs.numericValue)")
Logger.coreData.debug("Oldest obs: \(oldestObs.timestamp) || \(oldestObs.numericValue)")
for parameter in parameters {
var twoMostRecentObservatonsForParameter = observations
.filter { $0.cd_Parameter == parameter }
.prefix(2)
if let last = twoMostRecentObservatonsForParameter
.first(where: { $0.timestamp > lastObservationTimeLimit }) {
lastObservation[parameter] = last.numericValue
twoMostRecentObservatonsForParameter.removeAll(where: { $0.objectID == last.objectID })
} else {
lastObservation[parameter] = nil
}
recentObservation[parameter] = twoMostRecentObservatonsForParameter
.first(where: { $0.timestamp > recentObservationTimeLimit })?.numericValue
}
}
// MARK: - NSFetchedResultsControllerDelegate conformance
internal func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
let newObservations = controller.fetchedObjects as? [Observation] ?? []
observations = newObservations
updateHistoricalObservations()
}
}
}
NSManagedObject subclass:
extension Observation {
// Computed properties excluded to aid clarity
class func create(in context: NSManagedObjectContext,
patient: Patient,
parameter: Parameter,
numericValue: Double? = nil,
stringValue: String? = nil) -> Observation {
precondition(!((numericValue != nil) && (stringValue != nil)), "No values sent to initialiser")
let observation = Observation(context: context)
observation.cd_Patient = patient
observation.timestamp = Date.now
observation.parameter = parameter
if let value = numericValue {
observation.numericValue = value
} else {
observation.stringValue = stringValue!
}
try! context.save()
return observation
}
static var fetchAll: NSFetchRequest<Observation> {
let request = Observation.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \Observation.cd_timestamp, ascending: true)]
return request
}
static func fetchAllDateSorted(for patient: Patient) -> NSFetchRequest<Observation> {
let request = fetchAll
request.sortDescriptors = [NSSortDescriptor(keyPath: \Observation.cd_timestamp, ascending: true)]
request.predicate = NSPredicate(format: "%K == %#", #keyPath(Observation.cd_Patient), patient)
return request
}
static func fetchDateSorted(for patient: Patient, and parameter: Parameter) -> NSFetchRequest<Observation> {
let patientPredicate = NSPredicate(format: "%K == %#", #keyPath(Observation.cd_Patient), patient)
let parameterPredicate = NSPredicate(format: "%K == %#", #keyPath(Observation.cd_Parameter), parameter)
let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [patientPredicate, parameterPredicate])
let request = fetchAll
request.predicate = compoundPredicate
return request
}
}
Console output: (note observation count increments but the most recent observation does not change)
There is something wrong with your timestamps and/or sorting, the oldest observation is 4 days newer than the newest one (and it is in the future!)
Joakim was on the money - the timestamps are indeed incorrect; the problem was not in the logic but an error in the code (maths error relating to the TimeInterval between datapoints) that generated data for testing purposes. Garbage in, garbage out...
A lesson to me to be more careful - precondition now added to the function that generated the time series data (and a unit test!).
static func placeholderTimeSeries(for parameter: Parameter, startDate: Date, numberOfValues: Int) -> [(Date, Double)] {
let observationTimeInterval: TimeInterval = (60*5) // 5 minute intervals, not 5 hours! Test next time!!
let observationPeriodDuration: TimeInterval = observationTimeInterval * Double(numberOfValues)
let observationEndDate = startDate.advanced(by: observationPeriodDuration)
precondition(observationEndDate < Date.now, "Observation period end date is in the future")
return placeholderTimeSeries(valueRange: parameter.referenceRange,
valueDelta: parameter.controlStep...(3 * parameter.controlStep),
numberOfValues: numberOfValues,
startDate: startDate,
dataTimeInterval: observationTimeInterval)
}
In view model I try to call two functions one of them create new list "addNewList" and in side this function I create a new sublist "addMainSublist" as shown in the code bellow, the "addNewList" function work perfectly. Also, when I try to call the "addMainSublist" inside "addNewList" the error in the photo shown inside "PersistenceController" file when I try to save the new sublists in CoreData, I call the "addNewList" inside SwiftUI view and take "moc" parameter from "#Environment(.managedObjectContext) private var managedObjectContext" property in the SwiftUI view.
Note: as you see in the code bellow I use the same "managedObjectContext" to save both new list and new sublist, when I erase all content in the simulator and try to create new list the error did not show but in the second try the error shown again.
func addNewList(moc: NSManagedObjectContext, mainLists: FetchedResults<ListOfTasks>, favoriteLists: FetchedResults<ListOfTasks>) {
let newList = ListOfTasks(context: moc)
newList.id = UUID()
newList.addedDate = addedDate
newList.title = title
newList.icon = icon
newList.color = Color.set(stringfor: color)
newList.origin_Group = origin_Group
if origin_Group == nil {
newList.state = ListState.list.rawValue
newList.index = Int16(mainLists.count)
newList.isFavorite = isFavorite
newList.favoriteIndex = Int16(isFavorite ? favoriteLists.count : 0)
}else{
if let wrappedGroup = origin_Group {
newList.state = ListState.sublist.rawValue
newList.index = Int16(wrappedGroup.listsArray.count-1)
newList.isFavorite = false
newList.favoriteIndex = 0
}
}
newList.isLocked = isLocked
newList.isArchived = isArchived
PersistenceController.shared.save()
addMainSublist(to: newList, moc: moc)
}
func addMainSublist(to newList: ListOfTasks, moc: NSManagedObjectContext) {
let mainSublist = Sublist(context: moc)
mainSublist.origin_List = newList
mainSublist.id = UUID()
mainSublist.addedDate = Date()
mainSublist.index = 0
mainSublist.title = "\(title)_MainSublist"
mainSublist.isExpanded = false
mainSublist.isArchived = false
PersistenceController.shared.save()
}
Error Photo
The answer by Joakim Danielson who gives me the note about "compare", the problem is using sort description "list.title" to fetch all sublists in the SwiftUI view for debugging, and when I change that the error is gone, thanks for "Joakim Danielson" for his valuable note.
Not the issue specifically here but in case on your search you come across this same error, check if you have your "one to many" or "one to one" relationship set correctly. That was my case where I forgot to change it to "one to many" when I set up the relationship between two entities. That solved my error.
So, I am fetching some data from CoreData and I am using NSSortDescriptor to sort it in ascending order (I've tried other methods too). What happens is that the lowercase string with same text comes first. As per my understanding, in ASCII, uppercase strings come first ('A' starting at 65) and then lowercase strings ('a' starting at 97) and if we consider this, uppercase strings should come first when sorting in ascending order.
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true, selector: #selector(NSString.caseInsensitiveCompare(_:)))
let entities = CoreDataManager.getData(entityName: "Entity", predicate: nil, sortDescriptor: sortDescriptor) as? [Entity]
In my CoreDataManager class, I have the following methods.
class CoreDataManager: NSObject {
static func getManagedObject() -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.managedObjectContext
}
static func getData(entityName: String, predicate: NSPredicate? = nil, sortDescriptor: NSSortDescriptor? = nil) -> [NSManagedObject] {
var resultsManagedObject: [NSManagedObject] = []
do {
let managedObject = getManagedObject()
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
if let descriptor = sortDescriptor {
request.sortDescriptors = [descriptor]
}
if predicate != nil {
request.predicate = predicate
}
let results = try managedObject.fetch(request)
resultsManagedObject = results as! [NSManagedObject]
} catch {
print("There was an error retrieving data")
}
return resultsManagedObject
}
}
I've also tried sorting using sorted method of arrays but that was also giving the same output as using NSSortDescriptor.
let arr = ["bamboo", "document", "BAMBOO", "DOCUMENT"]
print(arr.sorted { $0.lowercased() < $1.lowercased() }) // Output ~> ["bamboo", "BAMBOO", "document", "DOCUMENT"]
Why is sorting working like this? Is it the same in other languages?
It's not sorting lowercase first. It's comparing uppercase and lowercase as equal. But obviously, in an array, something has to come first. If a sorting algorithm is "stable", then if two elements compare equal, the one that appears first in the input will also appear first in the output. If the algorithm is "unstable" they might come out in any random order. It looks like you're getting a stable sort (or just getting lucky); since your example input has lowercase values before their corresponding uppercase values, the output does too.
I have the following core data model:
where Person to Codes is a one-to-many relationship.
I have a function which returns a Person record and if the code person.codes returns an NSSet of all the codes associated with that Person. The issue that I am having is how to use the NSSet.
person.codes.allObjects.first returns this data:
<Codes: 0x60000213cb40> (entity: Codes; id: 0xb978dbf34ddb849 <x-coredata://A2B634E4-E136-48E1-B2C5-82B6B68FBE44/Codes/p1> ; data: {
code = 4LQ;
number = 1;
whosAccount = "0xb978dbf34ddb869 <x-coredata://A2B634E4-E136-48E1-B2C5-82B6B68FBE44/Person/p1>";
})
I thought if I made person.codes.allObjects.first of type Codes, I would be able to access the code and number elements but I get an error: error: value of type 'Any?' has no member 'number'
Also, how can I search this data set for a particular code or number.
I appreciate that this is proabably a simple question but have searched and read the documentation to no avail. I suspect that may base knowledge is not sufficient.
Update
I have a CoreDataHandler class which contains the following code:
class CoreDataHandler: NSObject {
//static let sharedInstance = CoreDataHandler()
private static func getContext() -> NSManagedObjectContext {
let appDelegate = NSApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
static func fetchPerson() -> [Person]? {
let context = getContext()
do {
let persons: [Person] = try context.fetch(Person.fetchRequest())
return persons
} catch {
return nil
}
}
I can fetch a person using:
let row = personTableView.selectedRow
let person = CoreDataHandler.fetchPerson()?[row]
Core Data supports widely native Swift types.
Declare codes as Set<Codes> in the Person class.
It's much more convenient than typeless NSSet.
You get a strong type and you can apply all native functions like filter, sort, etc. without type cast.
let codes = person.codes as! Set<Code>
Once that is done you can access the properties. Searching can be done by filtering for instance
let filteredCodes = codes.filter({ $0.code == "XYZ" })
will return all objects that has the code "XYZ". Or to get only one you can use
let code = codes.first(where: {$0.id == 1})
which will return the first object that has id = 1
A simple example getting all Person objects that has a given code
func findWithCode(_ code: String) -> [Person] {
guard let persons = CoreDataHandler.fetchPerson() else {
return []
}
var result = [Person]()
for person in persons {
let codes = person.codes as! Set<Code>
if codes.contains(where: { $0.code == code }) {
result.append(person)
}
}
return persons
}
I have integrated the FMDB Sqlite Wrapper into my iOS application and I have written a couple of DB functions, that have all worked perfectly until now.
I used the same approach for a simple method that is supposed to return a single element in a Resultset item, but I get the above error. There is nothing different in the logic from the other methods so I have no idea why the error occurs and why it happens on both lines with stringForColumn and not in the line with intForColumn (as the debugger tells me).
Any help or tips would be greatly appreciated!
func fetchExercise(exerciseId: Int) -> Exercise {
sharedInstance.database!.open()
let resultSet: FMResultSet! = sharedInstance.database!.executeQuery("Select * from Exercises where ExerciseId = ?", withArgumentsInArray: [String(exerciseId)])
let fetchedExercise: Exercise = Exercise()
if (resultSet != nil) {
fetchedExercise.exerciseId = Int(resultSet.intForColumn("ExerciseId"))
fetchedExercise.exerciseCategory = resultSet.stringForColumn("ExerciseCategory")
fetchedExercise.exerciseTitle = resultSet.stringForColumn("ExerciseTitle")
}
sharedInstance.database!.close()
return fetchedExercise
}
The resultSet is as follows:
You have to call resultsSet.next() to retrieve the results. For example:
func fetchExercise(exerciseId: Int) -> Exercise {
sharedInstance.database!.open()
defer { sharedInstance.database!.close() }
let resultSet = try! sharedInstance.database!.executeQuery("Select * from Exercises where ExerciseId = ?", values: [exerciseId])
let fetchedExercise = Exercise()
if resultSet.next() {
fetchedExercise.exerciseId = resultSet.longForColumn("ExerciseId")
fetchedExercise.exerciseCategory = resultSet.stringForColumn("ExerciseCategory")
fetchedExercise.exerciseTitle = resultSet.stringForColumn("ExerciseTitle")
}
return fetchedExercise
}
BTW, I'm not sure why you're converting exerciseId to a string in the query. I'd personally just pass the Int value, as shown above.