Search code examples
iosswiftautolayout

Swift - Subviews frame.maxY reading incorrectly


I have a basic sign up screen set up programmatically with the UI elements inside a view that is itself inside a scroll view.

The last UI element in the screen is a register button. I set up a keyboard notification observer with the Will Show and Will Hide notifications.

I am running this code on iPod touch 7th gen simulator.

My problem is when trying to read the maxY value of the sign up button and compare it to the keyboard minY it prints wrong numbers.

The keyboard is clearly blocking the register button which mean the button's maxY value will be greater the the keyboard minY value.

However the values printed shows that there is something wrong with the reading of the register button frame.

Here is my code:

import UIKit

class RegisterVC: UIViewController {
    
    
    private let scrollView: UIScrollView = {
        let scroll = UIScrollView()
        scroll.clipsToBounds = true
        scroll.isScrollEnabled = true
        scroll.translatesAutoresizingMaskIntoConstraints = false
        scroll.showsVerticalScrollIndicator = false
        
        return scroll
    }()
    
    private let scrollInnerView: UIView = {
        let innerView = UIView()
        innerView.translatesAutoresizingMaskIntoConstraints = false
        
        return innerView
    }()
    
    private let profilePic: UIImageView = {
        let imageView = UIImageView()
        imageView.image = UIImage(systemName: "person.circle")
        imageView.contentMode = .scaleAspectFit
        imageView.tintColor = .gray
        imageView.translatesAutoresizingMaskIntoConstraints = false
        
        return imageView
    }()
    
    
    private let usernameField: UITextField = {
        let field = UITextField()
        field.autocapitalizationType = .none
        field.autocorrectionType = .no
        field.returnKeyType = .next
        field.layer.cornerRadius = 12
        field.layer.borderWidth = 1
        field.layer.borderColor = UIColor.lightGray.cgColor
        field.placeholder = "Username..."
        field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: 0))
        field.leftViewMode = .always
        field.backgroundColor = .white
        field.keyboardType = .default
        field.isHighlighted = false
        field.textAlignment = .left
        field.translatesAutoresizingMaskIntoConstraints = false
        
        return field
    }()
    
    private let emailField: UITextField = {
        let field = UITextField()
        field.autocapitalizationType = .none
        field.autocorrectionType = .no
        field.returnKeyType = .next
        field.layer.cornerRadius = 12
        field.layer.borderWidth = 1
        field.layer.borderColor = UIColor.lightGray.cgColor
        field.placeholder = "Email Address..."
        field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: 0))
        field.leftViewMode = .always
        field.backgroundColor = .white
        field.keyboardType = .default
        field.textAlignment = .left
        field.translatesAutoresizingMaskIntoConstraints = false
        
        return field
    }()
    
    private let passwordField: UITextField = {
        let field = UITextField()
        field.autocapitalizationType = .none
        field.autocorrectionType = .no
        field.returnKeyType = .done
        field.layer.cornerRadius = 12
        field.layer.borderWidth = 1
        field.layer.borderColor = UIColor.lightGray.cgColor
        field.placeholder = "Password..."
        field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: 0))
        field.leftViewMode = .always
        field.backgroundColor = .white
        field.isSecureTextEntry = true
        field.textAlignment = .left
        field.keyboardType = .default
        field.translatesAutoresizingMaskIntoConstraints = false
        
        return field
    }()
    
    private let registerButton: UIButton = {
        let button = UIButton()
        button.setTitle("Create Account", for: .normal)
        button.backgroundColor = .systemGreen
        button.setTitleColor(.white, for: .normal)
        button.layer.cornerRadius = 12
        button.layer.masksToBounds = true
        button.titleLabel?.font = .systemFont(ofSize: 20, weight: .bold)
        button.translatesAutoresizingMaskIntoConstraints = false
        
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        title = "Create Account"
        view.backgroundColor = .white
        
        view.addSubview(scrollView)
        scrollView.addSubview(scrollInnerView)
        scrollInnerView.addSubview(profilePic)
        scrollInnerView.addSubview(usernameField)
        scrollInnerView.addSubview(emailField)
        scrollInnerView.addSubview(passwordField)
        scrollInnerView.addSubview(registerButton)
        usernameField.delegate = self
        emailField.delegate = self
        passwordField.delegate = self
        profilePic.isUserInteractionEnabled = true
        registerButton.addTarget(self,
                                 action: #selector(registerButtonTapped),
                                 for: .touchUpInside)

        setUpKeyboard()
        setUpConstraints()
        
    }
    
    private func setUpConstraints() {
        
        // Scroll View Constraints
        
        scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
        scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
        
        // Scroll Inner View Constraints
        
        scrollInnerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
        scrollInnerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
        scrollInnerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
        scrollInnerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
        scrollInnerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
        scrollInnerView.heightAnchor.constraint(equalTo: scrollView.heightAnchor, constant: 1).isActive = true
        
        // Profile Picture Constraints
        
        profilePic.widthAnchor.constraint(equalTo: scrollInnerView.widthAnchor, multiplier: 1/3).isActive = true
        profilePic.heightAnchor.constraint(equalTo: scrollInnerView.widthAnchor, multiplier: 1/3).isActive = true
        profilePic.centerXAnchor.constraint(equalTo: scrollInnerView.centerXAnchor).isActive = true
        profilePic.topAnchor.constraint(equalTo: scrollInnerView.topAnchor, constant: 10).isActive = true
        
        // User Name Field Constraints
        
        usernameField.widthAnchor.constraint(equalTo: scrollInnerView.widthAnchor, constant: -60).isActive = true
        usernameField.heightAnchor.constraint(equalToConstant: 45).isActive = true
        usernameField.topAnchor.constraint(equalTo: profilePic.bottomAnchor, constant: 10).isActive = true
        usernameField.centerXAnchor.constraint(equalTo: profilePic.centerXAnchor).isActive = true
        
        // Email Field Constraints
        
        emailField.widthAnchor.constraint(equalTo: usernameField.widthAnchor).isActive = true
        emailField.heightAnchor.constraint(equalTo: usernameField.heightAnchor).isActive = true
        emailField.topAnchor.constraint(equalTo: usernameField.bottomAnchor, constant: 10).isActive = true
        emailField.centerXAnchor.constraint(equalTo: usernameField.centerXAnchor).isActive = true
        
        // Password Field Constraints
        
        passwordField.widthAnchor.constraint(equalTo: emailField.widthAnchor).isActive = true
        passwordField.heightAnchor.constraint(equalTo: emailField.heightAnchor).isActive = true
        passwordField.topAnchor.constraint(equalTo: emailField.bottomAnchor, constant: 10).isActive = true
        passwordField.centerXAnchor.constraint(equalTo: emailField.centerXAnchor).isActive = true
        
        // Register Button Constraints
        
        registerButton.widthAnchor.constraint(equalTo: passwordField.widthAnchor).isActive = true
        registerButton.heightAnchor.constraint(equalTo: passwordField.heightAnchor).isActive = true
        registerButton.topAnchor.constraint(equalTo: passwordField.bottomAnchor, constant: 20).isActive = true
        registerButton.centerXAnchor.constraint(equalTo: passwordField.centerXAnchor).isActive = true
    }
    
    private func setUpKeyboard() {
        
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowNotification), name: UIResponder.keyboardWillShowNotification, object: nil)
        
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideNotification), name: UIResponder.keyboardWillHideNotification, object: nil)
    }
    
    @objc private func keyboardWillShowNotification(_ notification: NSNotification) {
        
        guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
        else {
            
            return
        }
        
        print(keyboardSize.minY)
        print(registerButton.frame.maxY)
        
    }
}


Solution

  • It's because the keyboard frame and the button frame are in two different coordinate systems. You cannot compare them directly. You need to convert the button frame to window coordinates before comparing them. Or else convert the keyboard frame to the button frame coordinates (the button's superview).

    Actually what I typically do is convert the keyboard frame to the internal coordinates of the target view and compare that to the target view's bounds. For example:

    // n is the notification
    let d = n.userInfo!
    var r = d[UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
    r = self.slidingView.convert(r, from:nil) // <- this is the key move!
    let h = self.slidingView.bounds.intersection(r).height
    

    That tells me whether the keyboard would cover the sliding view, and if so, by how much.