Search code examples
iosswiftuiviewnslayoutconstraint

Constraint's constant not updating in custom UIView's methods


I am trying to update a button's constraints when the keyboard is shown and hidden by adding/subtracting the keyboard's height from the constraint's constant.

I had this working previously, but after some re-factoring, it's stopped working. Previously, keyboardWillShow: and keyboardWillHide: were implemented exactly as shown below. I've since tried to use setNeedsUpdateConstraints and setNeedsLayout to try to force a refresh, to no avail.

When doing some simple print() debugging, buttonHorizontalConstraint.constant does get updated, but the changes just aren't reflected visually.

RegistrationNameView.swift

class RegistrationNameView: UIView {

    let questionLabel: UILabel = {
        let label = UILabel()

        label.font = UIFont.systemFontOfSize(21.0)
        label.text = "Hey, what's your name?"
        label.textAlignment = .Center
        label.textColor = UIColor.lightGrayColor()
        label.translatesAutoresizingMaskIntoConstraints = false

        return label
    }()
    let nameField: UITextField = {
        let field = UITextField()

        field.autocorrectionType = .No
        field.font = UIFont.boldSystemFontOfSize(28.0)
        field.placeholder = "Full name"
        field.returnKeyType = .Next
        field.textAlignment = .Center
        field.translatesAutoresizingMaskIntoConstraints = false

        return field
    }()
    let nextButton: UIButton = {
        let button = UIButton()

        button.setTitle("Continue", forState: .Normal)
        button.setTitleColor(UIColor.whiteColor(), forState: .Normal)
        button.titleLabel?.font = UIFont.boldSystemFontOfSize(17.0)
        button.backgroundColor = UIColor(red: 0.263, green: 0.910, blue: 0.847, alpha: 1)
        button.layer.cornerRadius = Global.UISizes.CornerRadius
        button.translatesAutoresizingMaskIntoConstraints = false
        button.contentEdgeInsets = UIEdgeInsetsMake(16.0, 0, 16.0, 0)

        return button
    }()

    var buttonHorizontalConstraint = NSLayoutConstraint()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.backgroundColor = UIColor.whiteColor()

        // Add subviews
        self.addSubview(questionLabel)
        self.addSubview(nameField)
        self.addSubview(nextButton)

        nameField.becomeFirstResponder()

        // Constraint helpers
        let spacer = Global.UISizes.SpacingUnit
        let margins = self.layoutMarginsGuide

        let layoutConstraints: [NSLayoutConstraint] = {
            var constraints = [NSLayoutConstraint]()

            // Title Label Constraints

            constraints.append(
questionLabel.bottomAnchor.constraintEqualToAnchor(nameField.topAnchor, constant: -(spacer * 2)))
            constraints.append(questionLabel.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor))
            constraints.append(questionLabel.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor))

            // Description Label Constraints
            constraints.append(nameField.topAnchor.constraintEqualToAnchor(margins.centerYAnchor, constant: spacer * -12))
            constraints.append(nameField.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor))
            constraints.append(nameField.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor))

            // Sign Up Button Constraints
            self.buttonHorizontalConstraint = nextButton.bottomAnchor.constraintEqualToAnchor(margins.bottomAnchor, constant: -(spacer * 2))
            constraints.append(self.buttonHorizontalConstraint)
            constraints.append(nextButton.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor))
            constraints.append(nextButton.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor))

            return constraints
        }()
        NSLayoutConstraint.activateConstraints(layoutConstraints)

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func keyboardWillShow(notification: NSNotification) {
        if let keyboardSize = (notification.userInfo! as NSDictionary).objectForKey(UIKeyboardFrameBeginUserInfoKey)?.CGRectValue.size {
            self.buttonHorizontalConstraint.constant -= keyboardSize.height
        }
    }

    func keyboardWillHide(notification: NSNotification) {
        if let keyboardSize = (notification.userInfo! as NSDictionary).objectForKey(UIKeyboardFrameBeginUserInfoKey)?.CGRectValue.size {
            self.buttonHorizontalConstraint.constant += keyboardSize.height
        }
    }

}

RegistrationNameViewController.swift

class RegistrationNameViewController: UIViewController {

    var nameView: RegistrationNameView! { return self.view as! RegistrationNameView }

    override func viewDidLoad() {
        super.viewDidLoad()

        let nameView = RegistrationNameView(frame: CGRectZero)
        nameView.nextButton.addTarget(self, action: "nextStep:", forControlEvents: .TouchUpInside)
        self.view = nameView
    }

    func nextStep(sender: AnyObject) {
        print("going to the next step \(sender)")

        let credentialsViewController = RegistrationCredentialsViewController()
        self.navigationController?.pushViewController(credentialsViewController, animated: true)
    }

}

RegistrationNavigationController.swift

class RegistrationNavigationController: UINavigationController, UINavigationControllerDelegate {

    var nameViewController: RegistrationNameViewController = RegistrationNameViewController()
    var credViewController: RegistrationCredentialsViewController = RegistrationCredentialsViewController()

    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)

        self.delegate = self
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {

        let type = String(viewController.dynamicType)

        switch type {
        case "RegistrationNameViewController":
            // Add keyboard notifications to RegistrationNameViewController
            NSNotificationCenter.defaultCenter().addObserver(nameViewController.view,
                selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
            NSNotificationCenter.defaultCenter().addObserver(nameViewController.view,
                selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)

        case "RegistrationCredentialsViewController":
            // Remove keyboard notifications to RegistrationNameViewController before
            // registering for new notifications
            NSNotificationCenter.defaultCenter().removeObserver(nameViewController.view)

            // Add keyboard notifications to RegistrationCredentialsViewController
            NSNotificationCenter.defaultCenter().addObserver(credViewController.view,
                selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
            NSNotificationCenter.defaultCenter().addObserver(credViewController.view,
                selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)

        default:
            print("Default")
        }

    }

}

Thank you for the help!


Solution

  • I solved this on my own after some digging.

    I believe the problem is that the NSNotificationCenter observer registrations were happening on a background thread, which caused the methods that actually impacted UI to not actually change the UI.

    Instead of registering observers in navigationController:willShowViewController:animated:, I register them in viewWillAppear and unregister them in viewWillDisappear. This happens in RegistrationNameViewController instead of RegistrationNavigationController.

    RegistrationNameViewController.swift

    override func viewWillAppear(animated:         
        NSNotificationCenter.defaultCenter().addObserver(self.view,
                selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
        NSNotificationCenter.defaultCenter().addObserver(self.view,
                selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)
    
        (self.view as! RegistrationNameView).nameField.becomeFirstResponder()
    }
    
    override func viewWillDisappear(animated: Bool) {
        NSNotificationCenter.defaultCenter().removeObserver(self.view)
    }
    

    This makes navigationController:willShowViewController:animated: unnecessary, and it can be removed from RegistrationNavigationController.swift