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()
}
}
Related
class TopViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//Code Block 1
let controller = getTopController()
print(controller)// Prints out MyTestProject.TopViewController
//Code Block 2
let controller2 = getRootController()
print(controller2)//Prints out nil , because keywindow is also nil upto this point.
//Code Block 3
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
let controller2 = self.getRootController()
print(controller2)// Prints out MyTestProject.TopViewController
}
}
func getTopController() -> UIViewController? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let sceneDelegate = windowScene.delegate as? SceneDelegate else {
return nil
}
return sceneDelegate.window?.rootViewController
}
func getRootController() -> UIViewController? {
let keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
let topController = keyWindow?.rootViewController
return topController
}
}
Since iOS 13 there is two approach to get current active / top view controller of the app.
here:
getTopController() and getRootController() shows both of the approaches.
As commented in codes besides print() results are different though.
In Code Block 2:
getRootController can't find the window yet so it prints out nil. Why is this happening?
Also, which is the full proof method of getting reference to top controller in iOS 13, I am confused now?
The problem is that when your view controller viewDidLoad window.makeKey() has not been called yet.
A possible workaround is to get the first window in the windows array if a key window is not available.
func getRootController() -> UIViewController? {
let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
let topController = keyWindow?.rootViewController
return topController
}
Please note that this will solve your problem but you should postpone any operation that involve using a key window until it is such.
According to the documentation of UIView, the window property is nil if the view has not yet been added to a window which is the case when viewDidLoad is called.
Try to access that in viewDidAppear
override func viewDidAppear(_ animated: Bool) {
let controller2 = self.view.window.rootViewController
}
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
}
I am having this problem for quite awhile, I have a slider menu use third party library, and I have an option to move to different indexSelected for the tab bar but it is in the different class.
AppDelegate.swift
var main: FirstViewController = FirstViewController()
func getMain() -> FirstViewController{
return main
}
SliderMenuViewController.swift
slideMenuController()?.closeLeftNonAnimation()
DispatchQueue.main.asyncAfter(deadline: .now() + 1 ) {
let abc: AppDelegate = UIApplication.shared.delegate as! AppDelegate
let tst = abc.getMain()
tst.moveTab()
FirstViewController.swift
func moveTab(){
DispatchQueue.main.async {
self.tabBarController?.selectedIndex = 2
}
}
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.)
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 :)