UI API called on a background thread error [duplicate] - ios

This question already has answers here:
-[UIApplication delegate] must be called from main thread only
(2 answers)
Closed 4 years ago.
I have a function that gets the context for storing to Core Data. I have it for a few view controllers, however I am getting an error.
private class func getContext() -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.weatherPersistentContainer.viewContext
}
However I get an error about accessing UI API being called from a background thread.

As you see from the error, you can't call UIApplication.shared from a background thread. Since you don't want to have to wrap every call to our getContext method in DispatchQueue.main.async, you can update your getContext method to do the necessary wrapping as needed:
private class func getContext() -> NSManagedObjectContext {
let appDelegate: AppDelegate
if Thread.current.isMainThread {
appDelegate = UIApplication.shared.delegate as! AppDelegate
} else {
appDelegate = DispatchQueue.main.sync {
return UIApplication.shared.delegate as! AppDelegate
}
}
return appDelegate.weatherPersistentContainer.viewContext
}
This code ensures that UIApplication.shared is only called on the main queue, even if getContext is called from a background thread. The nice part is that the result is returned on the original thread.

Related

Managed Object Context is nil for some reason in iOS

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.

UIApplication.delegate must be used from main thread only [duplicate]

This question already has answers here:
-[UIApplication delegate] must be called from main thread only
(2 answers)
Closed 5 years ago.
I have the following code in my app delegate as a shortcut for working with CoreData in my other viewControllers:
let ad = UIApplication.shared.delegate as! AppDelegate
let context = ad.persistentContainer.viewContext
However, I now get the error message:
"UI API called from background thread" and "UIApplication.delegate must be used from main thread only".
I am working with CoreData while my app is in the background, but this is the first time I've seen this error message. Does anybody know what's going on here?
Update: I tried to move this inside the appDelegate class itself, and using the following code -
let dispatch = DispatchQueue.main.async {
let ad = UIApplication.shared.delegate as! AppDelegate
let context = ad.persistentContainer.viewContext
}
Now, I can no longer access the ad and context variables outside the AppDelegate. Is there something I'm missing?
With ref to this (-[UIApplication delegate] must be called from main thread only) in Swift (for your query resolution)
DispatchQueue.main.async(execute: {
// Handle further UI related operations here....
//let ad = UIApplication.shared.delegate as! AppDelegate
//let context = ad.persistentContainer.viewContext
})
With edit: (Where is the correct place to declare ad and context? Should I declare these in my viewControllers in the main dispatch)
Place of variables (ad and context) declaration defines scope for it. You need to decide what would be scope of these variable. You can declare them Project or application level (Globally), class level or particular this function level.
If you want to use these variable in other ViewControllers then declare it globally or class level with public/open/internal access control.
var ad: AppDelegate! //or var ad: AppDelegate?
var context: NSManagedObjectContext! //or var context: NSManagedObjectContext?
DispatchQueue.main.async(execute: {
// Handle further UI related operations here....
ad = UIApplication.shared.delegate as! AppDelegate
context = ad.persistentContainer.viewContext
//or
//self.ad = UIApplication.shared.delegate as! AppDelegate
//self.context = ad.persistentContainer.viewContext
})

Thread safe way to get currently displayed view controller

Currently I use this method to get the current view controller:
func topMostContoller()-> UIViewController?{
if !Thread.current.isMainThread{
logError(message: "ACCESSING TOP MOST CONTROLLER OUTSIDE OF MAIN THREAD")
return nil
}
let topMostVC:UIViewController = UIApplication.shared.keyWindow!.rootViewController!
return topVCWithRootVC(topMostVC)
}
This method goes through the hierarchy of the view controllers starting at the rootViewController until it reaches the top.
func topVCWithRootVC(_ rootVC:UIViewController)->UIViewController?{
if rootVC is UITabBarController{
let tabBarController:UITabBarController = rootVC as! UITabBarController
if let selectVC = tabBarController.selectedViewController{
return topVCWithRootVC(selectVC)
}else{
return nil
}
}else if rootVC.presentedViewController != nil{
if let presentedViewController = rootVC.presentedViewController! as UIViewController!{
return topVCWithRootVC(presentedViewController)
}else{
return nil
}
} else {
return rootVC
}
}
This issue is in topMostController since it uses UIApplication.shared.keyWindow and UIApplication.shared.keyWindow.rootViewController which should not be used in a background thread. And I get these warning:
runtime: UI API called from background thread: UIWindow.rootViewController must be used from main thread only
runtime: UI API called from background thread: UIApplication.keyWindow must be used from main thread only
So my question is. Is there a thread safe way to access the currently displayed view controller?
Will accessing from the main thread suit your needs?
func getTopThreadSafe(completion: #escaping(UIViewController?) -> Void) {
DispatchQueue.main.async {
let topMostVC: UIViewController = UIApplication.shared.keyWindow?.rootViewController?
completion(topMostVC)
}
}
this can get a little bit confusing, since it's an asynchronous method, but my gut tells me that this'd be the safest option with whatever you're up to :)

UILabel.text must be used from main thread only [duplicate]

When I using Swift4in Xcode 9 gives me
UIApplication.delegate must be used from main thread only
.... must be used from main thread only
UI API called from background thread Group
Purple Warning.
My codes;
var appDelegate = UIApplication.shared.delegate as! AppDelegate
public var context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let prefs:UserDefaults = UserDefaults.standard
var deviceUUID = UIDevice.current.identifierForVendor!.uuidString
Warning line is;
public var context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
Another warning like this;
let parameters = [
"tel": "\(self.phone.text!)"
] as [String : String]
Gives
UITextField.text must be used from main thread only
Same error again..
How can I fix it ? Any idea ?
You're making this call on a background queue. To fix, try something like…
public var context: NSManagedObjectContext
DispatchQueue.main.async {
var appDelegate = UIApplication.shared.delegate as! AppDelegate
context = appDelegate.persistentContainer.viewContext
}
Although this is a pretty bad way to do this… you're using your App Delegate as a global variable (which we all know is bad!)
You should look at passing the managed object context from view controller to view controller…
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:
(window?.rootViewController as? MyViewController)?.moc = persistentContainer.viewContext
}
and so on

How do I refactor my code to call AppDelegate on the main thread?

I recently started migrating my project from Swift3/Xcode8 to Swift4/Xcode9. My app crashes at runtime because the main thread sanitizer allows access to UIApplication.shared.delegate only on the main thread, resulting in a crash at startup. I have the following code, which worked fine in Swift 3 -
static var appDelegate: AppDelegate {
return UIApplication.shared.delegate as! AppDelegate;
}
Other classes in my code make access to appDelegate. I need to figure out a way to return UIApplication.shared.delegate from the main thread.
Note: Using an DispatchQueue.main.async{} block from wherever access is made to appDelegate is not an option. Need to use it only within the static var appDelegate declaration only.
Looking for a clever workaround.
Relevant crash message:
Main Thread Checker: UI API called on a background thread: -[UIApplication delegate]
PID: 1094, TID: 30824, Thread name: (none), Queue name: NSOperationQueue 0x60400043c540 (QOS: UNSPECIFIED), QoS: 0
Solved by using Dispatch Groups.
static var realDelegate: AppDelegate?;
static var appDelegate: AppDelegate {
if Thread.isMainThread{
return UIApplication.shared.delegate as! AppDelegate;
}
let dg = DispatchGroup();
dg.enter()
DispatchQueue.main.async{
realDelegate = UIApplication.shared.delegate as? AppDelegate;
dg.leave();
}
dg.wait();
return realDelegate!;
}
and call it somewhere else for
let appDelegate = AppDelegate(). realDelegate!
I use the following:
static var shared: AppDelegate? {
if Thread.isMainThread {
return UIApplication.shared.delegate as? AppDelegate
}
var appDelegate: AppDelegate?
DispatchQueue.main.sync {
appDelegate = UIApplication.shared.delegate as? AppDelegate
}
return appDelegate
}

Resources