Home > database >  weak optional variable showing nil even after assigning value?
weak optional variable showing nil even after assigning value?

Time:12-29

I have a LoginCoordinator class whose start() method instantiates the LoginViewController. But even after assigning the coordinator variable in LoginViewController from LoginCoordinator, 'sometimes' the coordinator variable is nil in the view-controller.

By 'sometimes' I mean, when I debug and keep a breakpoint inside the start() method then, the value is assigned.

This is my LoginCoordinator

class LoginCoordinator: Coordinator {
var navigationController: UINavigationController
weak var parent: MainCoordinator?
var childCoordinators: [Coordinator]?


init(navController: UINavigationController) {
    self.navigationController = navController
}

func start() {
    let vc: LoginViewController = UIStoryboard.main.instantiateViewController()
    vc.coordinator = self
    navigationController.pushViewController(vc,animated: true)
}

func goToSignUp() {
    let signUpCoordinator = SignUpCoordinator(navController: navigationController)
    childCoordinators?.append(signUpCoordinator)
    signUpCoordinator.start()
}

func goToHomeVC() {
    let homeCoordinator = HomeCoordinator(navControl: navigationController)
    childCoordinators?.append(homeCoordinator)
    homeCoordinator.start()
 }
}

This is my LoginViewController

class LoginViewController: UIViewController {

@IBOutlet weak private var userName: HubTextField!
@IBOutlet weak private var password: HubTextField!
@IBOutlet weak private var forgotPassword: UILabel!
@IBOutlet weak var loginButton: HubButton!
var viewModel: LoginSignupVM = .init(totalItem: 2)
weak var coordinator: LoginCoordinator?

fileprivate func setupForgotPassword() {
    forgotPassword.isUserInteractionEnabled = true
    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(forgotTapped))
    forgotPassword.addGestureRecognizer(tapGesture)
}

override func viewDidLoad() {
    super.viewDidLoad()
    loginButton.appearance = Appearance(cornerRadius: loginButton.frame.height/2)
    loginButton.setTitle(TitleText.loginText, for: .normal)
    loginButton.isEnabled = false
    setupTextField()
    hideKeyboardWhenTappedAround()
    setupForgotPassword()
    setupNavigationBar()
    
    // MARK: remove this later
    userName.text = "[email protected]"
    password.text = "asdfg123"
}

private func setupNavigationBar() {
    let titleView = UILabel(frame: .zero)
    titleView.text = "Login Up"
    titleView.font = UIFont.systemFont(ofSize: 20)
    navigationItem.titleView = titleView
    rightButton()
}

fileprivate func rightButton() {
    let rightButton = UIBarButtonItem(title: "signUp", style: .plain, target: self, action: #selector(goToSignUPScreen))
    rightButton.title = "sign Up"
    navigationItem.rightBarButtonItem = rightButton
}

@objc private func goToSignUPScreen() {
    coordinator?.goToSignUp()
}

@objc private func backButtonClicked() {
    navigationController?.popViewController(animated: true)
}

@objc private func forgotTapped() {
    //MARK: todo forgot password
}

private func assignTags() {
    userName.tag = 0
    password.tag = 1
}

private func setupTextField() {
    userName.placeHolder = TitleText.email
    userName.textFieldType = .email
    userName.delegate = self
    password.placeHolder = TitleText.password
    password.textFieldType = .password
    password.delegate = self
    assignTags()
}

private func goToHomeViewController() {
    coordinator?.goToHomeVC()
}

@IBAction func onLoginClick(_ sender: Any) {
    //MARK: todo login click change hardcoded verification
    if let email = userName.text, let password = password.text {
        if email == "[email protected]" && password == "asdfg123" {
            goToHomeViewController()
        }
    }
  }
}

extension LoginViewController: ValidatorButton {
func addNewValidated(tag: Int, result: ValidationResult) {
    viewModel.validationList[tag] = result
    loginButton.isEnabled = viewModel.loginButtonEnabled()
   }
  }

LoginCoordinator is called from MainCoordinator as:

class MainCoordinator: Coordinator {
var isLogin: Bool = false
var navigationController: UINavigationController
var childCoordinators: [Coordinator]? = []
let window: UIWindow?

init(navController: UINavigationController, window: UIWindow) {
    self.navigationController = navController
    self.window = window
}

func start() {
    if isLogin {
       let child = HomeCoordinator(navControl: navigationController)
        childCoordinators?.append(child)
        child.start()
    } else {
    let child = LoginCoordinator(navController: 
navigationController)
        childCoordinators?.append(child)
        child.start()
    }
   } 
}

And MainCoordinator is called from sceneDelegate as follows:

   func scene(_ scene: UIScene, willConnectTo session: 
UISceneSession, options connectionOptions: 
UIScene.ConnectionOptions) {
    guard let scene = (scene as? UIWindowScene) else { return }
    let window = UIWindow(windowScene: scene)
    self.window = window
    
    let navigationController = UINavigationController()
    let mainCoordinator = MainCoordinator(navController: 
  navigationController,window: window)
    mainCoordinator.start()
    window.rootViewController = navigationController
    window.makeKeyAndVisible()
  }

CodePudding user response:

mainCoordinator is declared as a local variable in the willConnectTo delegate method:

let mainCoordinator = MainCoordinator(navController: 
    navigationController,window: window)

This means that it can be deallocated after the last access to it in willConnectTo.

Since mainCoordinator holds the only strong reference to LoginCoordinator, it will be deallocated too. This is why you see nil in the view controller - at that point, mainCoordinator has been deallocated.

You still see a non-nil value in LoginCoordinator.start, because at that point mainCoordinator.start still hasn't finished, so mainCoordinator is not deallocated yet.

To solve this, you can either make the scene delegate hold a strong reference to the main coordinator, by declaring a property called mainCoordinator:

var mainCoordinator: MainCoordinator!

// in willConnectTo:
mainCoordinator = MainCoordinator(navController: 
    navigationController,window: window)
  • Related