Search code examples
iosswiftseguealamofireuistoryboard

Present a view controller from a detached file from view controller


I have this app that makes use of Alamofire for sending requests to a backend that spits back a response and according to the response the app preforms an action. I find the Alamofire code to be too cluttering for my project and I’d like to parse the code to a one line code. To do that I put the Alamofire block of code in a class called Requests in one of my custom framework libraries. Every time I get a response from the backend I’d like to run a function (not a problem) and then do a segue/present/push to the next view controller (the problem).

Let’s say I have a Request class which contains a login() function which takes in a username and password parameters. The one line code when this function is called would look like: Request.login(username, password) and eventually Request.logout(), etc. This function can be found in ACommonLibrary framework found in the same Xcode project. This will run the Alamofire code to send the request and then get response saying “authenticated: true” in JSON format and according to that the app will segue/present /push to the Profile view controller.

I tried doing this with a boolean where if I get a response a variable will be set from false to true and returned by the function so I can use it in an if/else statement, but that didn’t work. I tried it normally with

let storyboard:UIStoryboard = UIStoryboard(name: "login", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "Login") as! LoginViewController
self.present(vc, animated: true, completion: nil)

but I get Instance member ‘present' cannot be used on type 'Request' Then I found someone asking How to present a view controller from a detached view controller? but that didn’t work either, assuming it’s because I have multiple view controllers in one storyboard.

I searched for this error and other similar ones to no avail. So now I’m questioning is this even possible to achieve or do I have to approach it differently? Any suggestions are more than appreciated. Thanks in advance.

EDIT: Added code below.

Main
-> Login
-> -> LoginViewController.swift

class LoginViewController: UIViewController {

    @IBOutlet weak var usernameTextField: CustomTextField!
    @IBOutlet weak var passwordTextField: CustomTextField!
    @IBOutlet weak var loginButton: UIButton!
    @IBOutlet weak var forgotPasswordButton: UIButton!
    @IBOutlet weak var navigationBar: UINavigationBar!

    override func viewWillAppear(_ animated: Bool) {
        UIApplication.shared.statusBarStyle = .lightContent
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // MARK: - IBActions
    @IBAction func loginPressed(sender: AnyObject) {
        usernameTextField.trimSpaces()

        let username = usernameTextField.text!
        let password = passwordTextField.text!

        Request.login(username, password)
    }
}

ACommonLibrary
-> Backend
-> -> Request.swift

public class Request {
    public class func login(_ username:String, _ password:String) {

    let headers: HTTPHeaders = [
        "Content-Type": "application/x-www-form-urlencoded"
    ]

    let parameters:Parameters = [
        "username": username,
        "password": password
    ]

    let endpoint = GlobalVariables.BaseURLString + "login"

    Alamofire.request(endpoint, method: .post, parameters: parameters, headers: headers)
    .responseJSON { response in
        switch response.result {
            case .success/*(let data)*/:
            if response.result.value != nil{
                let swiftyJsonVar = JSON(response.result.value!)

                let responseSuccess = swiftyJsonVar["success"]
                let responseMessage = swiftyJsonVar["error_message"]
                let responseSession = swiftyJsonVar["session_id"]
                let responseUserId = swiftyJsonVar["user_id"]

            if responseSession != JSON.null {
                GlobalVariables.Keychain["user_id"] = responseUserId.rawValue as? String
                GlobalVariables.Keychain["session_id"] =  responseSession.rawValue as? String
                try? GlobalVariables.Keychain.set(username, key: "username")

                if responseSuccess.boolValue {
                    let storyboard:UIStoryboard = UIStoryboard(name: "profile", bundle: nil)
                    let vc = storyboard.instantiateViewController(withIdentifier: "ProfileNavigation") as!     ProfileViewController
                    self.present(vc, animated: true, completion: nil)
                }else{
                    SCLAlertView().showNotice("Warning!", subTitle: responseMessage.stringValue)
                }
            }else{
                SCLAlertView().showError("Error logging in", subTitle: "There was an error while logging in     to your account. If this error persists, the server is down :D ")
            }
        }
        case .failure/*(let err)*/:
//          NSLog("------------------DATA START-------------------")
//          NSLog("Response String: (String(describing: err))")
            SCLAlertView().showError("Error logging in", subTitle: "There was an error while logging in to     your account. If this error persists, the server is down :D ")
//          NSLog("------------------DATA END-------------------")
        }
    }
}    

Solution

  • First of all you should know that self is not accessible in class methods which you attempt to do in login method of Requests class. What you are trying to do is possible in two ways as explained below.

    1. Requests is not a subclass of UIViewController. Only a UIViewController or its subclass can present another view controller. So so can make use of the keyWindow and rootViewController of your shared application. Here's how:

      if responseSuccess.boolValue {
          let storyboard:UIStoryboard = UIStoryboard(name: "profile", bundle: nil)
          let vc = storyboard.instantiateViewController(withIdentifier: "ProfileNavigation") as! ProfileViewController
          let rootVC = UIApplication.shared.keyWindow?.rootViewController
          rootVC?.present(vc, animated: true, completion: nil)
      } else {
          SCLAlertView().showNotice("Warning!", subTitle: responseMessage.stringValue)
      }
      
    2. Use closures in the methods that make asynchronous network calls like you are doing in login method. And then perform segues and show/present view controllers in the closure. Here's how:

      public class Request {
      public class func login(_ username:String, _ password:String, withSuccess successHandler:@escaping (Bool) -> Void, andFailure failureHandler:@escaping (String) -> Void) {
      
          let headers: HTTPHeaders = ["Content-Type": "application/x-www-form-urlencoded"]
      
          let parameters:Parameters = ["username": username, "password": password]
      
          let endpoint = GlobalVariables.BaseURLString + "login"
      
          Alamofire.request(endpoint, method: .post, parameters: parameters, headers: headers)
              .responseJSON { response in
                  switch response.result {
                  case .success/*(let data)*/:
                      if response.result.value != nil{
                          let swiftyJsonVar = JSON(response.result.value!)
      
                          let responseSuccess = swiftyJsonVar["success"]
                          let responseMessage = swiftyJsonVar["error_message"]
                          let responseSession = swiftyJsonVar["session_id"]
                          let responseUserId = swiftyJsonVar["user_id"]
      
                          if responseSession != JSON.null {
                              GlobalVariables.Keychain["user_id"] = responseUserId.rawValue as? String
                              GlobalVariables.Keychain["session_id"] =  responseSession.rawValue as? String
                              try? GlobalVariables.Keychain.set(username, key: "username")
      
                              if responseSuccess.boolValue {
                                  successHandler(true)
                              } else {
                                  failureHandler(responseMessage.stringValue)
                              }
                          } else {
                              failureHandler("There was an error while logging in to your account. If this error persists, the server is down :D")
                          }
                      }
                  case .failure/*(let err)*/:
                      failureHandler("There was an error while logging in to your account. If this error persists, the server is down :D")
                  }
              }
          }
      }
      

      Then in the LoginViewController

      @IBAction func loginPressed(sender: AnyObject) {
          usernameTextField.trimSpaces()
      
          let username = usernameTextField.text!
          let password = passwordTextField.text!
      
          Request.login(username,
                        password,
                        withSuccess: { (success) in
                          let vc = self.storyboard?.instantiateViewController(withIdentifier: "ProfileViewController") as! ProfileViewController
                          self.present(vc, animated: true, completion: nil)
          }) { (errorMsg) in
              SCLAlertView().showError("Error logging in", subTitle: errorMsg)
          }
      }