Using present in UIViewController properly - swift

I encounter the problem that I cannot present a dynamic UIViewController in my class using "self". It tells me "Value of type '(LoginScreenVC) -> () -> (LoginScreenVC)' has no member 'view'".
It would work using a closure like e.g. if let loginScreen = UIStoryBoard ..., but since the UIViewController to switch to is dynamic, I cannot cast it to a specific UIViewController.
is there any other way to present the ViewController?
This is my code:
SWIFT 4.2 / XCode 10.1
import UIKit
class LoginScreenVC: UIViewController {
let myTokenHandler = TokenHandler()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func loginButton(_ sender: Any) {
print("Login button pressed")
let usernameInputField = self.view.viewWithTag(6548) as! UITextField
let passwordInputField = self.view.viewWithTag(6549) as! UITextField
userInput = usernameInputField.text!
passInput = passwordInputField.text!
// call completion handler
requestToken(success: handlerBlock)
}
// completion handler step 1: request token and get redirect string to switch screen
func requestToken(success: (String) -> Void) {
let requestResult = myTokenHandler.requestToken(password: passInput, username: userInput)
success(requestResult)
}
// completion handler step 2: use redirect string to switch screen
let handlerBlock: (String) -> Void = { redirect in
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let loginScreen = storyboard.instantiateViewController(withIdentifier: redirect)
self.present(loginScreen, animated: true, completion: nil) //Value of type '(LoginScreenVC) -> () -> (LoginScreenVC)' has no member 'view'
}
}

You are saying:
let handlerBlock: (String) -> Void = { redirect in
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let loginScreen = storyboard.instantiateViewController(withIdentifier: redirect)
self.present(loginScreen, animated: true, completion: nil)
}
The problem is that you are using the term self in a context where there is no self. (Well, there is a self, but it isn't what you think.) present is a UIViewController instance method, so self needs to be a UIViewController instance; and in this context, it isn't.
I can think of half a dozen ways to express the thing you're trying to express, but the simplest would probably be to rewrite that as:
func handlerBlock(_ redirect:String) -> Void {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let loginScreen = storyboard.instantiateViewController(withIdentifier: redirect)
self.present(loginScreen, animated: true, completion: nil)
}
Now handlerBlock is an instance method, and self is meaningful — it is the instance, which is just what you want. The rest of your code is unchanged, because the bare name handlerBlock in the expression requestToken(success: handlerBlock) is a function name, just as before.

Related

Prevent ViewController from stacking in the background when created with present

I use this code to open a new ViewController:
// Get a random next post
#IBAction func buttonNextPostTapped(_ sender: UIButton) {
let postNumber = Int.random(in: 0 ..< postIds.count)
let postId = postIds[postNumber]
PostApi.shared.getPost(postId: postId) { (post) in
let storyBoard : UIStoryboard = UIStoryboard(name: "MainApplication", bundle: nil)
let nextViewController = storyBoard.instantiateViewController(withIdentifier: "PostsViewController") as! PostsViewController
nextViewController.post = post
nextViewController.isFromRandom = true
self.present(nextViewController, animated: true, completion: {})
}
}
This code will open the same ViewController with different data. It works, however, the "old" ViewControllers will stack in the background. So if I open 10 new ViewControllers, I have 10 VC in the background.
How can I present a new ViewController, and dismiss the "old" one?
Using setViewControllers function from UINavigationController is the best way.
func setViewControllers(_ viewControllers: [UIViewController], animated: Bool)
And you can remove whichever controller you want to from stack like
if var navigationControllersArray:Array = (self.navigationController?.viewControllers) {
navigationControllersArray.remove(at: navigationControllersArray.count-2)
self.navigationController?.viewControllers = navigationControllersArray
}

Data passing with reusable popup VC function

I am trying to pass data from a pop-up VC to the parent VC using reusable function to present the pop-up and get the result. I have tried using the closure method with a function variable in the pop-up, I get an error "UIViewController has no member 'onSave' (function variable). Any help would be appreciated. thx
I have used delegates and segues but would like to avoid so one function can be present the pop-up and collect the choices for many instances.
// Parent VC
class a2ExpenseAddViewController: UIViewController {
#IBAction func selectEtype(_ sender: Any) {
popTitle = "Expense Types"//global var
popMessage = "Select type below or add new" // global var
menuPopup(choices: eTypes){ result in guard let result = else{
return
}
self.eType.setTitle(result, for: .normal)
print("popChoice=",result)
}
}
extension UIViewController {
func menuPopup (choices: [String], completion: #escaping (_ choice: String?) -> ()) {
let main = UIStoryboard(name: "Main", bundle: nil)
let goto = main.instantiateViewController(withIdentifier: "popup")
popChoices = choices
// Error below here - UIViewController has no member 'onSave'
goto.onSave = { (data: String) -> () in
choice = data
}
self.present(goto, animated: true, completion: nil)
}
//Popup VC
PopupViewController: UIViewController,.. // storyboard id is 'popup'
var onSave: ((_ data: String) -> ())?
#IBAction func add(_ sender: Any) {
popChoice = choice.text!
onSave?(popChoice)
dismiss(animated: true) }
I expect to assign result via onSave(data) to a button title.
I get an error "UIViewController has no member 'onSave' (function variable).
Cast the ViewController to specific class type after instantiation.
let goto = main.instantiateViewController(withIdentifier: "popup") as? PopupViewController
Then you should be able to do this
goto.onSave = { (data: String) -> () in
choice = data
}

Swift: Can't use navigationController.pushViewController inside a function

I am transitioning between views programatically using the code below and it gets repeated quite a lot so I wanted to create a global function but I can't seem to be able to get the hang of it.
The code works when called inside a ViewController class, so I suppose the problem is that my function doesn't know which VC do I want to call navigationController.pushViewController on, but I don't know how to reference the VC either as an argument passed to the function, or better yet with something like .self to take the current VC class the function is called in.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "ExamplesControllerVC")
self.navigationController?.pushViewController(vc, animated: true)
The error I get if I try to run that as a function in a separate file is:
Use of unresolved identifier 'navigationController'; did you mean 'UINavigationController'?
So the function I'd like to create and call is something like:
showVC("ExamplesControllerVC")
Any ideas?
Whatever function this code is in needs to be updated to take a parameter of type UIViewController.
func showMain(on vc: UIViewController) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "ExamplesControllerVC")
vc.navigationController?.pushViewController(vc, animated: true)
}
Now you can call this as:
showMain(on: someViewController)
Or add this function to an extension on UIViewController then your use of self works just fine.
extension UIViewController {
func showMain() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "ExamplesControllerVC")
self.navigationController?.pushViewController(vc, animated: true)
}
}
Do you want to do something like this?:
extension UIViewController {
func presentView(withIdentifier: String) {
if let newVC = self.storyboard?.instantiateViewController(withIdentifier: withIdentifier) {
self.present(newVC, animated: true, completion: nil)
}
}
}
You can call it this way:
self.presentView(withIdentifier: "yourIdentifier")

Is it possible to make the compiler see VC name (from string) as that VC type?

I'm trying the make a function that receives a name (String) of a View Controller and then presents that View Controller modally. Example:
func presentViewControllerModally(newViewControllerName: String){
let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let newViewController = storyBoard.instantiateViewController(withIdentifier: "\(newViewControllerName)_Identifier") as! newViewControllerName
self.present(newViewController, animated: false, completion: nil)
}
But I can't make the as! newViewControllerName to work as if the newViewControllerName is the type of the newViewControllerName and not the object itself.
You don’t have to downcast controller to your custom subclass. You can just present UIViewController.
If you have to get the real viewController type in return, just use generics to do this.
func presentViewControllerModally<T: UIViewController>(newViewControllerName: String) -> T? {
let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
if let newViewController = storyBoard.instantiateViewController(withIdentifier: "\(newViewControllerName)_Identifier") as? T {
self.present(newViewController, animated: false, completion: nil)
return newViewController
}
return nil
}
Then call
let myVC: MyVController = presentViewControllerModally(newViewControllerName: "MyVC")
If you don't need the right type but just UIViewController, you don't need a cast.

'Could not find a storyboard named 'MainTabController' in bundle NSBundle

the error I'm receiving that I can't seem to fix is
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: 'Could not find a storyboard
named 'MainTabController' in bundle NSBundle
the app will build and the login screen will display but crashes immediately after with the error stated above.
I have tried all of the following from other post similar to this and have had no luck.
Removing the reference to the storyboard in the info.plist file. When I do this the app does start but I get a black screen as it doesn't load the storyboard.
Fiddle with the Target Membership Main.storyboard.
Remove the storyboard from the project, clean, run and then adding the storyboard back again.
Uninstall Xcode, reinstall Xcode.
Deleting the Derived Data folder.
the problem appears to be with my presentMainScreen() method
func presentMainScreen(){
let mainstoryboard = UIStoryboard(name: "MainTabController", bundle: nil)
let mainTabController = mainstoryboard.instantiateViewController(withIdentifier: "MainTabController") as! MainTabController
mainTabController.selectedViewController = mainTabController.viewControllers?[1]
//let storyboard:UIStoryboard = UIStoryboard(name:"Main", bundle:nil)
//let loggedInVC:LoggedInVC = storyboard.instantiateViewController(withIdentifier: "LoggedInVC") as! LoggedInVC
//self.present(loggedInVC, animated: true, completion: nil)
}
if I comment out the mainTabController lines the app will work perfectly, also if I uncomment the loggedInVC lines and with the mainTabController lines commented out it works perfectly as well.
any suggestions are greatly appreciated.
below is my entire ViewController.swift code and a screenshot of my workspace
workspace
ViewController.swift
import UIKit
import FirebaseAuth
class ViewController: UIViewController {
#IBOutlet weak var emailTextField: UITextField!
#IBOutlet weak var passwordTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
if Auth.auth().currentUser != nil {
print("success")
self.presentMainScreen()
}
}
#IBAction func creatAccountTapped(_ sender: Any) {
if let email = emailTextField.text, let password = passwordTextField.text {
Auth.auth().createUser(withEmail: email, password: password, completion:{ user, error in
if let firebaseError = error {
print(firebaseError.localizedDescription)
return
}
print("success")
self.presentMainScreen()
})
}
}
#IBAction func loginButtonTapped(_ sender: Any) {
if let email = emailTextField.text, let password = passwordTextField.text {
Auth.auth().signIn(withEmail: email, password: password, completion: { (user, error) in
if let firebaseError = error {
print(firebaseError.localizedDescription)
return
}
print("success")
self.presentMainScreen()
})
}
}
func presentMainScreen(){
let mainstoryboard = UIStoryboard(name: "MainTabController", bundle: nil)
let mainTabController = mainstoryboard.instantiateViewController(withIdentifier: "MainTabController") as! MainTabController
mainTabController.selectedViewController = mainTabController.viewControllers?[1]
//let storyboard:UIStoryboard = UIStoryboard(name:"Main", bundle:nil)
//let loggedInVC:LoggedInVC = storyboard.instantiateViewController(withIdentifier: "LoggedInVC") as! LoggedInVC
//self.present(loggedInVC, animated: true, completion: nil)
}
}
What is the name of your storyboard? Is it MainTabController.storyboard or Main.storyboard?
You are trying to load a storyboard named "MainTabController":
let mainstoryboard = UIStoryboard(name: "MainTabController", bundle: nil)
But previously you called it Main.storyboard:
Fiddle with the Target Membership Main.storyboard.
Also if the main tab controller is in the same storyboard as your login view controller then you can try to use this:
let mainTabController = self.storyboard.instantiateViewController(withIdentifier: "MainTabController")
Your story board is named as Main.storyboard. MainTabController is the controller in the Main storyboard.
let mainstoryboard = UIStoryboard(name: "Main", bundle: nil)
let mainTabController = mainstoryboard.instantiateViewController(withIdentifier: "MainTabController") as! MainTabController
mainTabController.selectedViewController = mainTabController.viewControllers?[1]
let mainstoryboard = UIStoryboard(name: "MainTabController", bundle: nil)
Your storyboard is named "Main"