I'm using Alamofire to submit a request to an endpoint using Swift. I parse the JSON objects that I receive from the response using the Codable protocol, and then try to insert the objects into Core Data using Managed Object Subclasses. However, when I do this, I keep receiving an error saying that my parent Managed Object Context (MOC) is nil. This doesn't make sense to me because I set the MOC via Dependency Injection from the AppDelegate, and confirm that it has a value by printing out it's value to the console in the viewDidLoad() method.
Here is my relevant code:
I set my MOC here:
class ViewController: UIViewController {
var managedObjectContext: NSManagedObjectContext! {
didSet {
print("moc set")
}
}
override func viewDidLoad() {
super.viewDidLoad()
print(managedObjectContext)
}
///
func registerUser(userID: String, password: String) {
let parameters: [String: Any] = ["email": userID, "password": password, "domain_id": 1]
let headers: HTTPHeaders = ["Accept": "application/json"]
Alamofire.request(registerURL, method: .patch, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseJSON { response in
switch response.result {
case .success:
if let value = response.result.value {
print(value)
let jsonDecoder = JSONDecoder()
do {
let jsonData = try jsonDecoder.decode(JSONData.self, from: response.data!)
print(jsonData.data.userName)
print(jsonData.data.identifier)
print(self.managedObjectContext)
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = self.managedObjectContext
let user = UserLogin(context: privateContext)
user.userName = jsonData.data.userName
user.domainID = Int16(jsonData.data.identifier)
user.password = "blah"
do {
try privateContext.save()
try privateContext.parent?.save()
} catch let saveErr {
print("Failed to save user", saveErr)
}
} catch let jsonDecodeErr{
print("Failed to decode", jsonDecodeErr)
}
}
case .failure(let error):
print(error)
}
}
}
The specific error message I'm getting is:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Parent NSManagedObjectContext must not be nil.'
I realize that Alamofire is download the data on a background thread, which is why I use a child context, but I'm not sure why the parent is nil.
Here is the setup code for my Managed Object Context:
class AppDelegate: UIResponder, UIApplicationDelegate {
var persistentContainer: NSPersistentContainer!
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
createContainer { container in
self.persistentContainer = container
let storyboard = self.window?.rootViewController?.storyboard
guard let vc = storyboard?.instantiateViewController(withIdentifier: "RootViewController") as? ViewController else { fatalError("Cannot instantiate root view controller")}
vc.managedObjectContext = container.viewContext
self.window?.rootViewController = vc
}
return true
}
func createContainer(completion: #escaping(NSPersistentContainer) -> ()) {
let container = NSPersistentContainer(name: "Test")
container.loadPersistentStores { _, error in
guard error == nil else { fatalError("Failed to load store: \(error!)") }
DispatchQueue.main.async { completion(container) }
}
}
Can anyone see what it is I'm doing wrong?
I don't see anything immediately "wrong" so lets debug this a bit.
Put a breakpoint in applicationDidFinish... right after the guard.
Put a breakpoint at the creation of the privateContext.
Which fires first?
Where is the registerUser function? In a view controller? I hope not :)
the breakpoint right after my guard statement fires first. And yes, my registerUser function is indeed inside a ViewController.
Putting network code in view controllers is a code smell. View Controllers have one job, manage their views. Data gathering belongs in a persistence controller; for example, extending your NSPersistentContainer and putting data collection code there.
However, that is not the issue here, just a code smell.
Next test.
Is your persistent container and/or viewContext being passed into your view controller and bring retained?
Is your view controller being destroyed before the block fires?
To test this, I would put an assertion before Alamofire.request and crash if the context is nil:
NSAssert(self.managedObjectContext != nil, #"Main context is nil in the view controller");
I would also put that same line of code just before:
privateContext.parent = self.managedObjectContext
Run again. What happens?
I ran the test as you described, and I get the error: Thread 1: Assertion failed: Main context is nil in the view controller
Which assertion crashed? (probably should change the text a bit...)
If it is the first one then your view controller is not receiving the viewContext.
If it is the second one then the viewContext is going back to nil before the block executes.
Change your assumptions accordingly.
discovered something that is relevant here: If I place a button to call the registerUser() function at the user's discretion instead of calling it directly from the viewDidLoad() method, there is no crash, the code runs fine, and the MOC has a value
That leads me down the theory that your registerUser() was being called before your viewDidLoad(). You can test that by putting a break point in both and see which one fires first. If your registerUser() fires first, look at the stack and see what is calling it.
If it fires after your viewDidLoad() then put a breakpoint on the context property and see what is setting it back to nil.
So if I remove that line, how do I set the MOC property on my RootViewController via Dependency Injection?
The line right before it is the clue here.
let storyboard = self.window?.rootViewController?.storyboard
Here you are getting a reference to the storyboard from the rootViewController which is already instantiated and associated with the window of your application.
Therefore you could change the logic to:
(self.window?.rootViewController as? ViewController).managedObjectContext = container.viewContext
Although I would clean that up and put some nil logic around it :)
The problem I realize is that the MOC in the RootViewController is being used BEFORE the MOC is returned from the closure, and set in the AppDelegate. What do I do here?
This is a common synchronous (UI) vs. asynchronous (persistence) issue. Ideally your UI should wait until the persistence loads. If you load the persistent stores and then finish the UI after the stores have loaded it will resolve this issue.
Without a migration we are generally talking ms here rather than seconds.
BUT ... You want the same code to handle the UI loading whether it is milliseconds or seconds. How you solve that is up to you (design decision). One example is to continue the loading view until the persistence layer is ready and then transition over.
If you did that you could then subtly change the loading view if a migration is happening to inform the user as to why things are taking so long.
Related
I am attempting to make a to-do app with a controller for both "this week" and "next week." (Objects with a date in the current week will be shown in the thisWeek view controller, while objects with a date next week will be shown in the nextWeek view controller. However, sometimes objects will not appear in the table until the app restarts, and I'm not sure why this is happening or how to fix it.
In the thisWeek class (which is also my initial view controller), I initialize my database like so:
let config = Realm.Configuration(
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < 1 {
migration.enumerateObjects(ofType: ToDoListItem.className()) { (_, newItem) in
newItem?["notes"] = ""
newItem?["isReoccuring"] = false
}
}
})
lazy var realm = try! Realm(configuration: config)
This worked fine and there weren't any issues. However, I don't know how best to access this db from another controller (nextWeek). This was my first attempt: (which is what I also use in the object entry controller, where objects are first added to the db)
lazy var realm = try! Realm()
I also added a refresh() to each controller's viewDidLoad() with the intention of refreshing the table every time the view is loaded. But it didn't help every situation.
func refresh(){
data = sortContent(arg: realm.objects(ToDoListItem.self).map({ $0 }))
thisTable.reloadData()
}
The issue with this is that some objects do not appear in their proper place until I restart the app. Only then do all objects appear properly. For example:
something created in the nextWeek controller with a date in the current week will not appear until app restarts
when an object in created in nextWeek controller, then date is changed from next week to this week, again, needs an app restart
Things that do work include:
something created in the thisWeek controller with a date in the next week will appear immediately
when an object is created in thisWeek controller, then date is changed from this week to next week works immediately
I thought I needed a way to sync the different "instances" of the db, but there may be another issue that I just don't know about. Any insight would be helpful
Would be happy to share more code snippets if more information is needed.
UPDATE
This is in AppDelegate.swift:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let config = Realm.Configuration(
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < 1 {
migration.enumerateObjects(ofType: ToDoListItem.className()) { (_, newItem) in
newItem?["notes"] = ""
newItem?["isReoccuring"] = false
}
}
})
Realm.Configuration.defaultConfiguration = config
let realm = try! Realm()
return true
}
And everywhere I need to use realm (thisWeek and nextWeek controllers, etc), I use:
let realm = RealmService.shared.realm
Again, only certain functionalities are properly working
First the code in the section I initialize my database initializes that database var with a config.
lazy var realm = try! Realm(configuration: config)
The next section could be a 'different' realm as no config is specified
lazy var realm = try! Realm()
Please review Local Migrations, noting that a migration should occur when the app is first launched, not every time realm is accessed
Inside your application(application:didFinishLaunchingWithOptions:)
In other words, perform the migration on app launch (ensuring to include Realm.Configuration.defaultConfiguration = config) and then after that use
let realm = try! Realm()
to access a local realm.
You will probably not want to do this:
data = sortContent(arg: realm.objects(ToDoListItem.self).map({ $0 }))
Realm objects are live updating and that code casts the realm objects to an array which not only 'breaks' the link between the objects and their backing data, it also loads ALL of the objects into memory, defeating their lazily loading nature. This can overwhelm a device and cause a crash. See Realm Key Concepts and Read Characteristics
To answer the question;
I thought I needed a way to sync the different "instances" of the db
You don't. You should access a realm the same way every time. Objects created on different realms are tied to that realm and trying to access them from a different realm will cause a crash.
One solution is to use a Singleton pattern, sometimes called a RealmManager or RealmService (search here on SO for those key words). While there is some debate about singletons in Swift, it works for this case.
Another solution is to have a master controller which 'talks' to realm and then sub viewControllers get their connection to realm from the master viewController. The would be a simple matter of instantiating the sub viewController from the master viewController and passing and/or assigning a property in the sub viewController. So conceptually something like this.
MasterVC
func getRealm() -> Realm {
//return a realm instance
}
func createSubVC {
let mySubVC = SubViewController()
mySubVC.realm = gGetRealm()
}
But for simplicity and maintenance, I would suggest a singleton pattern for this use case. You could do something like this; create a file and put this code in it
import RealmSwift
func gGetRealm() -> Realm? {
do {
let realm = try Realm()
return realm
} catch let error as NSError {
print("Error!")
print(" " + error.localizedDescription)
let err = error.code
print(err)
let t = type(of: err)
print(t)
return nil
}
}
then whenever you need a realm instance in your app
func saveData() {
if let realm = gGetRealm() {
try! realm.write {
//save some data
}
}
or a RealmService singleton
import RealmSwift
class RealmService {
private init() {}
static let shared = RealmService()
lazy var realm: Realm = {
let realm = try! Realm()
return realm
}()
}
accessed like this
let realm = RealmService.shared.realm
I am trying to best follow MVC. Where is the 'correct' place to put my array of data? I am talking about the people array below. Should it be placed in the view controller below, or should it be part of a data service/data store class that is instantiated in the view controller (through dependency injection)? I have seen it done both places.
class ViewController: UIViewController {
var people: [Person] = []
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let context = appDelegate.persistentContainer.viewContext
let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
people = try? context.fetch(fetchRequest)
}
}
It's totally fine to store it inside viewController according to MVC. Although, you can move it to some separate source object, or dataProvider, but most of the time it's ok, and from your code it seems for me as it's okay.
I have few comments for you:
1) It's better to move out CoreData logic from AppDelegate - whether to separate singleton, or to service object (preferably, cause singletons are hard to test)
2) context.fetch is better to wrap inside do {} catch {} block most of times
3) You don't have to wait until viewWillAppear fires to fetch your data, at least from provided code
4) It's better to move logic of fetching inside some service with method like fetchPersons
I am using Core Data for the first time in a project made in XCode 8, swift 3. I have used background context (calling container.performBackgroundTask block..) to save the data and main context to fetch the data. When my app re-launches, the data I saved in the private background context is deleted.
Please tell me where I went wrong !!!
Here I call CoreDataManager class save context method in applicationDidEnterBackground and applicationWillTerminate methods of AppDelegate class:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
lazy var coreDataMgrInstance = CoreDataManager.sharedInstance
func applicationDidEnterBackground(_ application: UIApplication) {
coreDataMgrInstance.saveContext()
}
func applicationWillTerminate(_ application: UIApplication) {
coreDataMgrInstance.saveContext()
}}
Here is my Singleton class CoreDataManager to initiate NSpersistentContainer
class CoreDataManager: NSObject {
class var sharedInstance: CoreDataManager {
struct Singleton {
static let instance = CoreDataManager()
}
return Singleton.instance
}
private override init() {
super.init()
}
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "E_CareV2")
let description = NSPersistentStoreDescription() // enable auto lightweight migratn
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
func saveContext(){
print("saveContext")
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Failure to save main context \(nserror), \(nserror.userInfo)")
}
}}
Now this is the class where I save and fetch the data from Core Data
class SenderViewController: UIViewController {
var persistentContainer: NSPersistentContainer!
override func viewDidLoad() {
super.viewDidLoad()
persistentContainer = CoreDataManager.sharedInstance.persistentContainer
let results = self.fetchPersistentData(entityName: "School", withPredicate: nil)
print("results \n \(results)")
}
#IBAction func enterPressed(_ sender: Any) {
self.persistentContainer.performBackgroundTask({ (backgroundContext) in
backgroundContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
let schoolentity = NSEntityDescription.insertNewObject(forEntityName: "School", into: backgroundContext) as! School
schoolentity.schoolName = "ABC"
schoolentity.schoolWebSite = "http://anywebsite.com/"
do {
try backgroundContext.save()
} catch {
fatalError("Failure to save background context: \(error)")
}
})
}
func fetchPersistentData(entityName: String?, withPredicate: NSPredicate?) -> [NSManagedObject]{
let context = self.persistentContainer.viewContext
let request: NSFetchRequest<School> = School.fetchRequest()
let newentity = NSEntityDescription.entity(forEntityName: entityName!, in: context)
request.entity = newentity
request.returnsObjectsAsFaults = false
do {
let searchResults = try context.fetch(request) as [NSManagedObject]
return searchResults
} catch {
print("Error with request: \(error)")
}
return []
}
Actually the lightweight migrations are enabled by default as you can see on the screenshot
So you can safely delete these lines:
let description = NSPersistentStoreDescription() // enable auto lightweight migratn
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
After that everything should work.
There are two ways to use NSPersistentContainer - the simple way and the correct way. You are mixing them which is leading to problems. Apple is a little inconsistent on this in its documentation so it is understandable that you are confused.
The Simple Way - The simple way is only use the viewContext both for reading and for writing and only from the main thread. There are never any conflicts because core-data is assessed via a single thread. The problem with this approach is that you can't run anything in a background thread. So if you are importing a lot of data the UI will freeze. If you have a super simple app (a small task list?) this will work OK, but not something that I would ever recommend for a serious app. It is OK for a testing app for a beginner to learn core-data.
The Correct Way - the correct way is to NEVER write to the viewContext EVER. Apple documents this in NSPersistentContainer documentation (but also in its template creates a save method for the viewContext?!). In this setup all writes to core data MUST go through performBackgroundTask and you have to call save on that context before the end of the block. You also need an operation queue to make sure that there are no merge conflicts see NSPersistentContainer concurrency for saving to core data. This setup is a lot harder to do correctly. Objects from performBackgroundTask contexts cannot leave the block and objects from the viewContext cannot be used in the block. The complier doesn't help you with this so it is something that you always need to watch out for.
Your problem is mergePolicy. mergePolicy is evil. If you have conflicts in core-data you have already done something wrong and any merge policy will lose data. In the simple setup there are no conflicts because it is all on one thread. In the correct setup there is no conflicts because of the queue you created when using performBackgroundTask. The problem is that if you use BOTH performBackgroundTask and write the the viewContext you can get conflicts and you will lose data. Personally, I think it is better to have no mergePolicy and crash then to silently lose data.
I figured out what causes my data not to be persistent in Core Data. It was these below 4 lines of code that I put in persistentContainer definition for enabling LIGHTWEIGHT MIGRATION of models:
let description = NSPersistentStoreDescription() // enable auto lightweight migratn
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
When I deleted these lines, I became able to retain my data when the app re-launches.
I had written the above lines to enable Lightweight Migration to my model, but I did not change my model or created new version of the model, making the Core Data unable to search the destination model in NSBundle and thus unable to infer the Mapping.
I am still not sure, how that would delete my data but I will keep trying to figure this out too and comment when I get success in it... :)
I'm working with Swift 3 and I'd like to change my view from a function in my class when login succeed.
I've got a LoginViewController which contains this function:
static let sharedInstance = LoginViewController()
//...
func showNextView() {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
guard let eventVC = storyboard.instantiateViewController(
withIdentifier: "EventsTVC") as? EventsTableViewController else {
assert(false, "Misnamed view controller")
return
}
self.present(eventVC, animated: false, completion: nil)
}
In my class APIManager, I call this function inside my asynchronous method using Alamofire:
func processAuth(_ data: [String: String]) {
print("-- auth process started")
//... defining vars
Alamofire.request(tokenPath, method: .post, parameters: tokenParams, encoding: JSONEncoding.default, headers: tokenHeader)
.responseJSON { response in
guard response.result.error == nil else {
print(response.result.error!)
return
}
guard let value = response.result.value else {
print("No string received in response when swapping data for token")
return
}
guard let result = value as? [String: Any] else {
print("No data received or data not JSON")
return
}
// -- HERE IS MY CALL
LoginViewController.sharedInstance.showNextView()
print("-- auth process ended")
}
}
My console returns this error message:
-- auth process started 2017-03-18 20:38:14.078043 Warning: Attempt to present on
whose view is not in the window
hierarchy!
-- auth process ended
I think it's not the best practice to change my view when my asynchronous method has ended.
I don't know what I've got to do. Currently, this is the process:
User opens the app and the LoginViewController is displayed, if no token is saved (Facebook Login)
In the case where it has to login, a button "Login with Facebook" is displayed
When login succeed, I send Facebook data in my processAuth() function in my APIManager class
When my API returns me the token, I saved it and change the view to EventsTVC
I put in bold where the problem is. And I would like to know if it's the best practice in my case. If so, how to avoid my error message?
I hope I made myself understood. Thanks for your help!
What actually happens is that your singleton instance LoginViewController wants to present itself while not being in the view hierarchy. Let me explain it thoroughly:
class LoginViewController: UIViewController {
static let sharedInstance = LoginViewController()
func showNextView() {
...
// presentation call
self.present(eventVC, animated: false, completion: nil)
}
In this function you are calling present() from your singleton instance on itself. You have to call it from a view which is (preferably) on top of the view hierarchy stack. The solution would probably be not using a singleton on a VC in the first place. You should be instantiating and presenting it from the VC that is currently on the screen. Hope this helps!
I have a view controller that will show images from Flickr. I put Flickr API request methods in a separate class "FlickrAPIRequests", and now I need to update the image in the view controller when I get the data.
I choose to go with that using a protocol to eliminate the coupling of the two classes. How would I achieve that?
You could define a protocol and set up your FlickrAPIRequests class to take a delegate. I suggest another approach.
Set up your FlickrAPIRequests to have a method that takes a completion handler. It might look like this:
func downloadFileAtURL(_ url: URL, completion: #escaping DataClosure)
The FlickrAPIRequests function would take a URL to the file to download, as well as a block of code to execute once the file has downloaded.
You might use that function like this (In this example the class is called DownloadManager)
DownloadManager.downloadManager.downloadFileAtURL(
url,
//This is the code to execute when the data is available
//(or the network request fails)
completion: {
[weak self] //Create a capture group for self to avoid a retain cycle.
data, error in
//If self is not nil, unwrap it as "strongSelf". If self IS nil, bail out.
guard let strongSelf = self else {
return
}
if let error = error {
print("download failed. message = \(error.localizedDescription)")
strongSelf.downloadingInProgress = false
return
}
guard let data = data else {
print("Data is nil!")
strongSelf.downloadingInProgress = false
return
}
guard let image = UIImage(data: data) else {
print("Unable to load image from data")
strongSelf.downloadingInProgress = false
return
}
//Install the newly downloaded image into the image view.
strongSelf.imageView.image = image
}
)
I have a sample project on Github called Asyc_demo (link) that has uses a simple Download Manager as I've outlined above.
Using a completion handler lets you put the code that handles the completed download right in the call to start the download, and that code has access to the current scope so it can know where to put the image data once it's been downloaded.
With the delegation pattern you have to set up state in your view controller so that it remembers the downloads that it has in progress and knows what to do with them once they are complete.
Write a protocol like this one, or similar:
protocol FlickrImageDelegate: class {
func displayDownloadedImage(flickrImage: UIImage)
}
Your view controller should conform to that protocol, aka use protocol method(s) (there can be optional methods) like this:
class ViewController:UIViewController, FlickrImageDelegate {
displayDownloadedImage(flickrImage: UIImage) {
//handle image
}
}
Then in FlickrAPIRequests, you need to have a delegate property like this:
weak var flickrDelegate: FlickrImageDelegate? = nil
This is used in view controller when instantiating FlickrAPIRequests, set its instance flickrDelegate property to view controller, and in image downloading method,when you download the image, you call this:
self.flickrDelegate.displayDownloadedImage(flickrImage: downloadedImage)
You might consider using callback blocks (closures) in FlickrAPIRequests, and after you chew that up, look into FRP, promises etc :)
Hope this helps
I followed silentBob answer, and it was great except that it didn't work for me. I needed to set FlickrAPIRequests class as a singleton, so here is what I did:
protocol FlickrAPIRequestDelegate{
func showFlickrPhoto(photo: UIImage) }
class FlickrAPIRequests{
private static let _instance = FlickrAPIRequests()
static var Instance: FlickrAPIRequests{
return _instance
}
var flickrDelegate: FlickrAPIRequestDelegate? = nil
// Make the Flickr API call
func flickrAPIRequest(_ params: [String: AnyObject], page: Int){
Then in the view controller when I press the search button:
// Set flickrDelegate protocol to self
FlickrAPIRequests.Instance.flickrDelegate = self
// Call the method for api requests
FlickrAPIRequests.Instance.flickrAPIRequest(paramsDictionary as [String : AnyObject], page: 0)
And Here is the view controller's extension to conform to the protocol:
extension FlickrViewController: FlickrAPIRequestDelegate{
func showFlickrPhoto(photo: UIImage){
self.imgView.image = photo
self.prepFiltersAndViews()
self.dismissAlert()
}
}
When the api method returns a photo I call the protocol method in the main thread:
// Perform this code in the main UI
DispatchQueue.main.async { [unowned self] in
let img = UIImage(data: photoData as Data)
self.flickrDelegate?.showFlickrPhoto(photo: img!)
self.flickrDelegate?.setPhotoTitle(photoTitle: photoTitle)
}