Thread safe way to get currently displayed view controller - ios

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 :)

Related

Change View Controller from a background thread

I made a custom camera app and I used a background thread for a loop containing a delay. If the delay were in the main thread it would interrupt the AVCaptureSession.
I want to return to ViewController (home page of app) when my loop finishes.
func takeAllPictures() {
DispatchQueue.global(qos: .background).async {
let frequency = Double(self.pictureFreq)!
let x = UInt32(frequency)
let totalTimes = Double(self.pictureTotalTime)! //number of pics
var picsLeftCount = totalTimes
while picsLeftCount > 0{
sleep(x)
self.takePicture()
picsLeftCount = picsLeftCount - 1
}
self.goBackHome()
}
}
func goBackHome() {
let viewController = storyboard?.instantiateViewController(identifier: Constants.StoryBoard.viewController) as? ViewController
view.window?.rootViewController = viewController
view.window?.makeKeyAndVisible()
}
My loop works until it is time to goBackHome() where I get a fatal error saying I can only execute goBackHome() from the main thread.
Is there a way I can execute goBackHome() or change view controllers from the background thread?
I've tried using external functions, but they still run in the background thread.
You can try running the contents of the goBackHome function in the main thread like this:
func goBackHome() {
DispatchQueue.main.async {
let viewController = storyboard?.instantiateViewController(identifier: Constants.StoryBoard.viewController) as? ViewController
view.window?.rootViewController = viewController
view.window?.makeKeyAndVisible()
}
}

UIView.init() must be used from main thread only (when i segue to this controller)

Hello this is my controller class
class passwordViewController: UIViewController {
let load = UIActivityIndicatorView(style: .whiteLarge) // cause error
let passwordTextFiled:UITextField = { // cause error
let pass = UITextField()
pass.placeholder = ""
pass.addDoneButtonOnKeyboard()
pass.textColor = .gray
pass.textAlignment = NSTextAlignment.center
return pass
}()
let barLabel:UILabel = {
let bar = UILabel()
bar.text=""
bar.backgroundColor = Colors.yellow
return bar
}()
// there is more code here.i avoid to copy
}
when i run this controller directly it is okay no error. but when i segue from other controller here cause this error
UIView.init() must be used from main thread only
update 1 :
there is A controller with one button and the button segue to controller B
and this is my segue code :
DispatchQueue.main.async {
self.performSegue(withIdentifier: "passVC", sender: nil)
}
and i have B controller's code here
swift 4 and code 10
The error is telling you that you are creating the view controller from a background thread and that it must be created on the main thread (All UI work must be done on the main thread).
So when you are in the background thread and want to do UI work, you should use a dispatch queue call to run the code in the correct thread.
DispatchQueue.main.async {
// UI work here
}
So in your case, I imagine you are doing some network request to check authentication.
networkService.checkAuth() { auth in
// do whatever NON UI work you need to here
DispatchQueue.main.async {
// UI work here
}
}
DispatchQueue.main.async {
//do UIWork here
}

how can I instantiate a viewController with a containerView and it's containerView at the same time?

I want to instantiate a viewController with a container with the following:
let vc = self.storyboard?.instantiateViewController(withIdentifier: ContainerViewController") as? ContainerViewController
I also need a reference to the containerView so I try the following:
let vc2 = vc.childViewControllers[0] as! ChildViewController
The app crashes with a 'index 0 beyond bounds for empty NSArray'
How can I instantiate the containerViewController and it's childViewController at the same time prior to loading the containerViewController?
EDIT
The use case is for AWS Cognito to go to the signInViewController when the user is not authenticated. This code is in the appDelegate:
func startPasswordAuthentication() -> AWSCognitoIdentityPasswordAuthentication {
if self.containerViewController == nil {
self.containerViewController = self.storyboard?.instantiateViewController(withIdentifier: "ContainerViewController") as? ContainerViewController
}
if self.childViewController == nil {
self.childViewController = self.containerViewController!.childViewControllers[0] as! ChildViewController
}
DispatchQueue.main.async {
self.window?.rootViewController?.present(self.containerViewController!, animated: true, completion: nil)
}
return self.childViewController!
}
The reason I am instantiating the container and returning the child is that the return needs to conform to the protocol which only the child does. I suppose I can remove the container but it has functionality that I would have wanted.
Short answer: You can't. At the time you call instantiateViewController(), a view controller's view has not yet been loaded. You need to present it to the screen somehow and then look for it's child view once it's done being displayed.
We need more info about your use-case in order to help you.
EDIT:
Ok, several things:
If your startPasswordAuthentication() function is called on the main thread, there's no reason to use DispatchQueue.main.async for the present() call.
If, on the other hand, your startPasswordAuthentication() function is called on a background thread, the call to instantiateViewController() also belongs inside a DispatchQueue.main.async block so it's performed on the main thread. In fact you might just want to put the whole body of your startPasswordAuthentication() function inside a DispatchQueue.main.async block.
Next, there is no way that your containerViewController's child view controllers will be loaded after the call to instantiateViewController(withIdentifier:). That's not how it works. You should look for the child view in the completion block of your present call.
Next, you should not be reaching into your containerViewController's view hierarchy. You should add methods to that class that let you ask for the view you are looking for, and use those.
If you are trying to write your function to synchronously return a child view controller, you can't do that either. You need to rewrite your startPasswordAuthentication() function to take a completion handler, and pass the child view controller to the completion handler
So the code might be rewritten like this:
func startPasswordAuthentication(completion: #escaping (AWSCognitoIdentityPasswordAuthentication?)->void ) {
DispatchQueue.main.async { [weak self] in
guard strongSelf = self else {
completion(nil)
return
}
if self.containerViewController == nil {
self.containerViewController = self.storyboard?.instantiateViewController(withIdentifier: "ContainerViewController") as? ContainerViewController
}
self.window?.rootViewController?.present(self.containerViewController!, animated: true, completion: {
if strongSelf == nil {
strongSelf.childViewController = self.containerViewController.getChildViewController()
}
completion(strongSelf.childViewController)
}
})
}
(That code was typed into the horrible SO editor, is totally untested, and is not meant to be copy/pasted. It likely contains errors that need to be fixed. It's only meant as a rough guide.)

UITableView not updating when switching between tabs

Preface: I've tried adding tableView.reloadData() to viewWillAppear (...and viewDidLoad, viewDidAppear, etc.) of the UITableViewController that's not updating. I threw in setNeedsDisplay for S's & G's, too.
I have a UITabBarController with 3 tabs on it. Each tab is a TableViewController is backed by Core Data and is populated with NSManagedObjects from one NSManagedObjectContext.
In TableViewController1 I make changes to the cells, the tableView reloads properly and reflects the changes. If I click the tab for TableViewController2, the changes made on TVC1 aren't reflected.
The changes made on TVC1 are persisting between launches, as I see them on TVC2 when I close the app and relaunch it.
What am I missing? Any ideas would be greatly appreciated.
Update
Here's the code in question:
func markFavorite(sender: AnyObject) {
// Store the sender in case you need it later. Might not need this.
clickedFavoriteButton = sender as! UIButton
if resultsSearchController.active {
let indexPath = sender.tag
let selectedSound = self.filteredSounds[indexPath]
print("markFavorite's sender tag is \(indexPath)")
if selectedSound.favorite == 1 {
selectedSound.favorite = 0
} else {
selectedSound.favorite = 1
}
saveManagedObjectContext()
} else {
let indexPath = sender.tag
let selectedSound = self.unfilteredSounds[indexPath]
print("markFavorite's sender tag is \(indexPath)")
if selectedSound.favorite == 1 {
selectedSound.favorite = 0
} else {
selectedSound.favorite = 1
}
saveManagedObjectContext()
}
}
func saveManagedObjectContext() {
if managedObjectContext.hasChanges {
do {
try self.managedObjectContext.save()
} catch {
// catch error here
}
}
self.tableView.reloadData()
}
You should always use a NSFetchedResultsController to display Core Data items in a table view. It comes with delegate methods that update your table as the underlying data changes (even before saving).
To get started, examine the Xcode template (Master-Detail) implementation. Once you get the hang of it you will love it. Everything works pretty much out of the box.
You may have to trigger context.save() manually because core-data isn't saving the data right away.
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
runOnMain() {
do {
try! self.context.save()
} catch let error as NSError {
//Handle any upcoming errors here.
}
}
Its important to run the method on the main thread otherwise you will get an error.
this method should do the job:
func runOnMain(block: dispatch_block_t) {
if NSThread.isMainThread() {
block()
}else{
dispatch_sync(dispatch_get_main_queue(), block)
}
}
Please let me know if this approach worked for you.
You should not try to reload data at any point in view controller lifecycle. Instead create delegates for each tab bar controller, set them properly and call delegate methods only when something really change in your data source. If you are not familiar with delegation you can learn more about it here

How to reload UIViewController data from App Delegate

I have some methods that I am calling from the appDelegate to sync changes with persistent storage and iCloud.
The methods and the appDelegate work fine, my app syncs changes fine; however when the app is mergingChanges and persistentStoreDidChange I am trying to refresh the view controller data and change the view controller title to syncing.
I have tried changing the UIViewController title text and it does not change when merging or persistentStoreWillChange, also when using the reloadData() method for the view controller collection the app crashes with an unexpected nil when unwrapping an optional value.
The thing is the project has many tableview & colectionview all within a UITabController so I really need a refresh the data of the view controller in the window not just one specific view. Does anybody know how to refresh the viewcontroller data from the appDelegate ?
func mergeChanges(notification: NSNotification) {
NSLog("mergeChanges notif:\(notification)")
if let moc = managedObjectContext {
moc.performBlock {
moc.mergeChangesFromContextDidSaveNotification(notification)
self.postRefetchDatabaseNotification()
}
}
let vc = CollectionViewController()
let view = self.window?.rootViewController
vc.title = "Syncing"
view?.title = "Syncing"
}
func persistentStoreDidImportUbiquitousContentChanges(notification: NSNotification) {
self.mergeChanges(notification);
}
func storesWillChange(notification: NSNotification) {
NSLog("storesWillChange notif:\(notification)");
if let moc = self.managedObjectContext {
moc.performBlockAndWait {
var error: NSError? = nil;
if moc.hasChanges && !moc.save(&error) {
NSLog("Save error: \(error)");
} else {
// drop any managed objects
}
moc.reset();
}
let vc = CollectionViewController()
vc.title = "Syncing"
// reset UI to be prepared for a totally different
// don't load any new data yet.
}
}
func storesDidChange(notification: NSNotification) {
// here is when you can refresh your UI and
// load new data from the new store
let vc = CollectionViewController()
// vc.collectionView.reloadData()
NSLog("storesDidChange posting notif");
self.postRefetchDatabaseNotification();
}
For above functionality you can use NSNotification Fire that notification to multiple classes when you want to update .

Resources