Generic core data fetch request - swift

I am trying to create sort of a generic wrapper for simple core data fetches.
What I wanted to achieve, is, instead of writing multiple redundant methods that look like this:
func loadNSMOSubclass() -> [NSMOSubclass] {
let fetchRequest: NSFetchRequest<NSMOSubclass> = NSMOSubclass.fetchRequest()
do {
let result = try mainContext.fetch(fetchRequest)
return result
}
catch {
return []
}
}
I thought I could create a generic helper for that:
struct EntityLoader<T> where T: NSManagedObject {
func loadEntity() -> [T] {
let fetchRequest: NSFetchRequest<T> = T.fetchRequest()
do {
let mainContext = CoreDataState().mainContext
let result = try mainContext.fetch(fetchRequest)
return result
}
catch {
return []
}
}
}
However, at that point the compiler has a weird error:
Cannot convert value of type NSFetchRequest<NSFetchRequestResult> to specified type NSFetchRequest<T>
where the suggested solution is even stranger, since everything compiles when I do a casting:
let fetchRequest: NSFetchRequest<T> = T.fetchRequest() as! NSFetchRequest<T>
That might be ugly, but I could live with that. However when I run this code I am getting a fatal error:
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: 'executeFetchRequest:error:
<null> is not a valid NSFetchRequest.'
Am I doing something wrong, or are these just some Swift's constraints that are currently impossible to surpass?

Problem here is fetchRequest() is a auto-generated helper function and it's only available for your NSManagedObject sub classes. It's not available for your generic type. Therefore, instead of using that function you have to use it's implementation with your generic type like this,
replace this line,
let fetchRequest: NSFetchRequest<T> = T.fetchRequest()
with this,
let fetchRequest: NSFetchRequest<T> = NSFetchRequest<T>(entityName: String(describing: T.self))

Related

Error creating NSFetchRequest<T> in generic function

I want a generic function to return a CoreData object:
func getManagedObject<T: NSManagedObject>(context: NSManagedObjectContext) -> T
{
let fetchRequest: NSFetchRequest<T> = T.fetchRequest() // ERROR
...
}
But I get Cannot assign value of type 'NSFetchRequest<NSFetchRequestResult>' to type 'NSFetchRequest<T>.
I've tried adding a where, but doesn't help. This only adds a warning Redundant conformance constraint 'T': 'NSFetchRequestResult':
func getEntity<T: NSManagedObject>(context: NSManagedObjectContext) -> T where T:NSFetchRequestResult
What does work is:
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = T.fetchRequest()
But why didn't my first attempt work?

CoreData's performBackgroundTask method crashes

I wrote a function to fetch database in CoreData. this function will take a closure and run performBackgroundTask to fetch the data. Then, passing the result to the closure to run.
I wrote static properties in AppDelegate for me to access viewContext easily:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
static var persistentContainer: NSPersistentContainer {
return (UIApplication.shared.delegate as! AppDelegate).persistentContainer
}
static var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
// ...
}
The following is the function(not method) I wrote which crashed by using context:
func fetch<T>(fetchRequest: NSFetchRequest<T>, keyForOrder: String? = nil, format: String? = nil, keyword: String? = nil, handler: (([T]?)->Void)? = nil) where T:NSManagedObject, T: NSFetchRequestResult {
AppDelegate.persistentContainer.performBackgroundTask{(context: NSManagedObjectContext) in
if let format = format?.trimmingCharacters(in: .whitespacesAndNewlines),
!format.isEmpty,
let keyword = keyword?.trimmingCharacters(in: .whitespacesAndNewlines),
!keyword.isEmpty {
fetchRequest.predicate = NSPredicate(format: format, keyword)
}
if let keyForOrder = keyForOrder {
fetchRequest.sortDescriptors = [NSSortDescriptor(key: keyForOrder, ascending: true)]
}
guard let cats = try? context.fetch(fetchRequest) else { // crash
return
}
context.performAndWait(){ // crash
if let handler = handler {
handler(cats)
}
}
}
}
but if i replace context with AppDelegate.viewContext, the function won't crash:
func fetch<T>(fetchRequest: NSFetchRequest<T>, keyForOrder: String? = nil, format: String? = nil, keyword: String? = nil, handler: (([T]?)->Void)? = nil) where T:NSManagedObject, T: NSFetchRequestResult {
AppDelegate.persistentContainer.performBackgroundTask{(context: NSManagedObjectContext) in
if let format = format?.trimmingCharacters(in: .whitespacesAndNewlines),
!format.isEmpty,
let keyword = keyword?.trimmingCharacters(in: .whitespacesAndNewlines),
!keyword.isEmpty {
fetchRequest.predicate = NSPredicate(format: format, keyword)
}
if let keyForOrder = keyForOrder {
fetchRequest.sortDescriptors = [NSSortDescriptor(key: keyForOrder, ascending: true)]
}
guard let cats = try? AppDelegate.viewContext.fetch(fetchRequest) else { // crash
return
}
AppDelegate.viewContext.performAndWait(){ // crash
if let handler = handler {
handler(cats)
}
}
}
}
what is exactly going on?
thanks.
Here are some issues:
performBackgroundTask is already on the right thread for the context so there is no reason to call context.performAndWait and may lead to a deadlock or a crash.
The items fetched or created in a performBackgroundTask cannot leave that block under any circumstances. The context will be destroyed at the end of the block and the managedObjects will crash when it tries to access its context
Managing core-data thread safety can be difficult and I have found it a generally good practice to never pass or return managed objects to functions, unless the context of the object is explicit and clear. This is not an unbreakable rule, but I think it is a good rule of thumb when making your APIs.
performBackgroundTask is generally used for updates to core data. If you are only doing fetches you should use the viewContext. Doing a fetch on the background only to pass it to the main thread is generally a waste.
While in a performBackgroundTask block you cannot access the viewContext - neither for reading or for writing. If you do the app can crash any at time with confusing crash reports, even at a later time when you are not violating thread safety.
I don't know what the predicates that you are creating look like, but I have a strong feeling that they are wrong. This would cause a crash when fetching.
Overall I think that the function you created has little value. If all it is doing is a fetch then you should simply create the predicate and sort descriptors and fetch on the viewContext. If you insist on keeping the function, then remove the performBackgroundTask, fetch using the viewContext, return the results(instead of a callback) and only call it from the main thread.

Swift2: return a optional type object

I'm new in the swift2 world and I currently struggle with a simple function :
// Get all moves for the first category
func getMuscles() -> BodyPart {
let bpart:BodyPart?
if let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext {
do{
let fetchRequest = NSFetchRequest(entityName: "BodyPart")
let fetchResults = try managedObjectContext.executeFetchRequest(fetchRequest) as! [BodyPart]
bpart = fetchResults[0]
} catch let error as NSError {
print(error)
bpart = nil
}
}
return bpart
}
How can I solve this issue ? And what are the 'best-practices' in swift2 for defining a function ?
Thank you
EDIT 1
I've tried to change the signature of the method, but error is still here :
The question you should be asking yourself is whether getMuscles() must always return an object or if it's fine for it to return a nil.
By changing the method signature to func getMuscles() -> BodyPart?,
you're basically stating that a nil might be returned from that method,
thus solving your immediate compile time issue.
In that particular context, because you're fetching objects from CoreData,
it might be wise to allow getMuscles() to return a nil.
The way you define your functions (if they return optionals ?, or not) entirely depends on the calling code.
Change your method signature to :
func getMuscles() -> BodyPart?
But be careful while unwrapping the return value when the this function is being called.
Just return:
func getMuscles() -> BodyPart? { }
Thats nothing to do with SWIFT2.. The return type is expecting some value BodyPart not an optional value BodyPart?...But you are returning a optional value bpart
func getMuscles() -> BodyPart {
let bpart:BodyPart?
....
return bpart
}
If you want to return bpart as it is you need to create the return type as optional
func getMuscles() -> BodyPart? {
let bpart:BodyPart?
....
return bpart
}
or if you want to just return the value try this
func getMuscles() -> BodyPart {
let bpart:BodyPart = ()//initialize here dont make optional
....
return bpart
}

Subclass AFNetworking JSON Response Serialization to insert JSON response body in to error data in Swift 2

AFNetworking does not return the JSON response body when it fails (e.g. 400 status code), therefore you must subclass AFJSONResponseSerializer and fill in the error object with such. This is the recommended here and an example is here
Up until Swift 2, I used the following code to achieve such:
import Foundation
let JSONResponseSerializerWithDataKey : String = "JSONResponseSerializerWithDataKey"
let JSONResponseUndefinedAPIFailureReason : String = "UNKNOWN_ERROR"
class JSONResponseSerializerWithData: AFJSONResponseSerializer
{
override func responseObjectForResponse (response: NSURLResponse, data: NSData, error: AutoreleasingUnsafeMutablePointer
<NSError?>) -> AnyObject
{
var json : AnyObject = super.responseObjectForResponse(response, data: data, error: error) as AnyObject
if (error.memory? != nil)
{
var errorValue = error.memory!
var userInfo = errorValue.userInfo
if let errorDetail = json["detail"] as? String
{
userInfo![JSONResponseSerializerWithDataKey] = errorDetail
}
else
{
userInfo![JSONResponseSerializerWithDataKey] = JSONResponseUndefinedAPIFailureReason
}
error.memory = NSError(domain: errorValue.domain, code: errorValue.code, userInfo: userInfo)
}
return json
}
}
Start with Swift 2, a new type of Error handling was introduced.
The signature of the above function is now:
override func responseObjectForResponse(response: NSURLResponse!, data: NSData!) throws -> AnyObject
I am having trouble achieving the same as above inside a do-catch statement as it seems the failure does not invoke the catch statement, and thus there is no access to the error object. Further, new ErrorTypes are essentially empty and don't contain
This is what I've tried, but the catch statement is never called:
class JSONResponseSerializerWithData: AFJSONResponseSerializer
{
override func responseObjectForResponse(response: NSURLResponse!, data: NSData!) throws -> AnyObject
{
do
{
return try super.responseObjectForResponse(response, data: data)
}
catch
{
let nsError = (error as NSError)
var userInfo = nsError.userInfo
if let errorDetail = userInfo["detail"] as? String
{
userInfo[JSONResponseSerializerWithDataKey] = errorDetail
}
else
{
userInfo[JSONResponseSerializerWithDataKey] = JSONResponseUndefinedAPIFailureReason
}
throw NSError(domain: nsError.domain, code: nsError.code, userInfo: userInfo)
}
}
}
I've tried stepping through the AFNetworking2 Library an the body of the response is there, so I could sub-class it in Objective-C rather than Swift, but would prefer doing such if possible.
Am I handling this incorrectly with a do-catch statement? Any pointers in the right direction would be greatly appreciated.
After more digging, I have found that the issue is as described here.
The error is not thrown, only filled, therefore catch is never called. To induce this old behaviour you can add "NS_SWIFT_NOTHROW" to the end of the Objective-C signature in the header file as described here
This will change the signature to:
override func responseObjectForResponse(response: NSURLResponse?, data: NSData?, error: NSErrorPointer) -> AnyObject?
Which can then be used like before.

Using Xcode 7 for 1st time and I'm getting an extra argument error call with the following code -

Getting extra argument error call in relation to the 'items' constant below. Has this changed following Xcode 7.0?
override func viewDidLoad() {
super.viewDidLoad()
let fm = NSFileManager.defaultManager()
let path = NSBundle.mainBundle().resourcePath!
let items = fm.contentsOfDirectoryAtPath(path, error: nil)
for item in items as! [String] {
if item.hasPrefix("nssl") {
objects.append(item)
}
}
According to Apple for contentsOfDirectoryAtPath:
In Swift, this method returns a nonoptional result and is marked with
the throws keyword to indicate that it throws an error in cases of
failure.
You call this method in a try expression and handle any errors in the
catch clauses of a do statement, as described in Error Handling
in The Swift Programming Language (Swift 2).
The new/correct way to do this in Swift 2.0 is this:
let fm = NSFileManager.defaultManager()
let path = NSBundle.mainBundle().resourcePath!
do {
let items = try fm.contentsOfDirectoryAtPath(path)
for item in items as! [String] {
if item.hasPrefix("nssl") {
objects.append(item)
}
}
}
catch let error as NSError {
error.description
}
You need to adopt to new error handling techniques added in Swift 2.0. You need to use do..try..catch for fixing this issue (error handling)
Change your code like:
override func viewDidLoad()
{
super.viewDidLoad()
let fm = NSFileManager.defaultManager()
let path = NSBundle.mainBundle().resourcePath!
do
{
let items = try fm.contentsOfDirectoryAtPath(path)
for item in items
{
if item.hasPrefix("nssl")
{
objects.append(item)
}
}
}
catch
{
// Handle error here
}
}
Also you don't need to use items as! [String], because fm.contentsOfDirectoryAtPath returns an array of strings [String]
Refer Swift Error Handling for more details. Also check contentsOfDirectoryAtPath:error: for checking that new method syntax.
My bet is, it's because in Swift 2 error handling changed. The method you are calling no longer takes the second "error" parameter, and instead "throws".
Read the Swift 2 docs.
You need to convert to the new do-try-catch construct. Xcode should have suggested it for you...?