skip to Main Content

I am trying to implement passcode screen, but I am having trouble with alignment, as you can see in this picture.

enter image description here

What I’m trying to do is, have three buttons in each row, so it actually looks like a "keypad". I am not quite sure how could I do this. I thought about making inside of first stack view which is vertical, four others horizontal stack views, but couldn’t manage to do it. Any suggestion or help would be appreciated. Thanks 🙂

Code is below.

class ViewController: UIViewController {

var verticalStackView: UIStackView = {
    var verticalStackView = UIStackView()
    verticalStackView.translatesAutoresizingMaskIntoConstraints = false
    verticalStackView.axis = .vertical
    verticalStackView.distribution = .fillEqually
    verticalStackView.spacing = 13
    verticalStackView.alignment = .fill
    verticalStackView.contentMode = .scaleToFill
    verticalStackView.backgroundColor = .red
    return verticalStackView
}()

var horizontalStackView: UIStackView = {
    var buttons = [PasscodeButtons]()
    var horizontalStackView = UIStackView(arrangedSubviews: buttons)
    horizontalStackView.translatesAutoresizingMaskIntoConstraints = false
    horizontalStackView.axis = .horizontal
    horizontalStackView.distribution = .fillEqually
    horizontalStackView.alignment = .fill
    horizontalStackView.spacing = 25
    horizontalStackView.contentMode = .scaleToFill
    horizontalStackView.backgroundColor = .green
    return horizontalStackView
}()

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .white
    configureStackView()
    configureConstraints()
}

func configureStackView() {
    view.addSubview(verticalStackView)
    verticalStackView.addSubview(horizontalStackView)
    addButtonsToStackView()
}

func addButtonsToStackView() {
    let numberOfButtons = 9
    for i in 0...numberOfButtons {
        let button = PasscodeButtons()
        button.setTitle("(i)", for: .normal)
        button.tag = i
        horizontalStackView.addArrangedSubview(button)
    }
}

func configureConstraints() {
    verticalStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 200).isActive = true
    verticalStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 50).isActive = true
    verticalStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -50).isActive = true
    verticalStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -100).isActive = true
    
    horizontalStackView.topAnchor.constraint(equalTo: verticalStackView.topAnchor, constant: 10).isActive = true
    horizontalStackView.leadingAnchor.constraint(equalTo: verticalStackView.leadingAnchor, constant: 10).isActive = true
  }
}

In case PasscodeButtons matters, here is code from there too.

class PasscodeButtons: UIButton {


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

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

override func awakeFromNib() {
    super.awakeFromNib()
    setupButton()
}

private func setupButton() {
    setTitleColor(UIColor.black, for: .normal)
    setTitleColor(UIColor.black, for: .highlighted)
}

private func updateView() {
    layer.cornerRadius = frame.width / 2
    layer.masksToBounds = true
    layer.borderColor = UIColor(red: 0/255.0, green: 0/255.0, blue: 0, alpha:1).cgColor
    layer.borderWidth = 2.0
}

override func layoutSubviews() {
    super.layoutSubviews()
    updateView()
    backgroundColor = .cyan
  }
}

2

Answers


  1. follow the steps:-

    • take a vertical stack view and add three buttons in it (for first line buttons)
    • take a vertical stack view and add three buttons in it (for second line buttons)
    • take a vertical stack view and add three buttons in it (for third line buttons)
    • take a vertical stack view and add three buttons in it (for fourth line buttons)
    • take a horizontal stackview and add all these 4 stackviews in it.
    Login or Signup to reply.
  2. The general idea is:

    • need 4 horizontal stack view "button rows" … 3 rows with 3 buttons each plus one row with 1 button (the "Zero" button)
    • create a vertical stack view to hold the "rows" of buttons
    • set all stack view distributions to .fillEqually
    • set all stack view spacing to the same value

    Then, to generate everything, create an array of arrays of Ints for the key numbers, laid out like a keypad:

        let keyNums: [[Int]] = [
            [7, 8, 9],
            [4, 5, 6],
            [1, 2, 3],
            [0],
        ]
    

    Loop through, creating each row of buttons.

    Here’s a quick example (I modified your PasscodeButton class slightly):

    class PasscodeButton: UIButton {
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            setupButton()
        }
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setupButton()
        }
        
        override func awakeFromNib() {
            super.awakeFromNib()
            setupButton()
        }
        
        private func setupButton() {
            setTitleColor(UIColor.black, for: .normal)
            setTitleColor(UIColor.lightGray, for: .highlighted)
            layer.masksToBounds = true
            layer.borderColor = UIColor(red: 0/255.0, green: 0/255.0, blue: 0, alpha:1).cgColor
            layer.borderWidth = 2.0
            backgroundColor = .cyan
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            layer.cornerRadius = bounds.height * 0.5
        }
        
    }
    
    class PassCodeViewController: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let outerStack = UIStackView()
            outerStack.axis = .vertical
            outerStack.distribution = .fillEqually
            outerStack.spacing = 16
            
            let keyNums: [[Int]] = [
                [7, 8, 9],
                [4, 5, 6],
                [1, 2, 3],
                [0],
            ]
            
            keyNums.forEach { rowNums in
                let hStack = UIStackView()
                hStack.distribution = .fillEqually
                hStack.spacing = outerStack.spacing
                rowNums.forEach { n in
                    let btn = PasscodeButton()
                    btn.setTitle("(n)", for: [])
                    // square / round (1:1 ratio) buttons
                    //  for all buttons except the bottom "Zero" button
                    if rowNums.count != 1 {
                        btn.heightAnchor.constraint(equalTo: btn.widthAnchor).isActive = true
                    }
                    btn.addTarget(self, action: #selector(numberTapped(_:)), for: .touchUpInside)
                    hStack.addArrangedSubview(btn)
                }
                outerStack.addArrangedSubview(hStack)
            }
            
            outerStack.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(outerStack)
            
            // respect safe area
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                outerStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
                outerStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                outerStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                // no bottom or height constraint
            ])
            
        }
        
        @objc func numberTapped(_ sender: UIButton) -> Void {
            guard let n = sender.currentTitle else {
                // button has no title?
                return
            }
            print("Number (n) was tapped!")
        }
        
    }
    

    Output:

    enter image description here

    You’ll likely want to play with the sizing, but that should get you on your way.


    Edit – comment "I would like for 0 to stay in last row in the middle, and on the left side I would pop in touch id icon and on the right backspace button, how could I leave last row out of a shuffle?"

    When you create your "grid" of buttons:

    • create the top three "rows" but leave the button titles blank.
    • create the "bottom row" of 3 buttons
      • set first button with "touchID" image
      • set title of second button to "0"
      • set third button with "backSpace" image
    • then call a function to set the "number" buttons

    Change the keyNums array to:

        let keyOrder: [Int] = [
            7, 8, 9,
            4, 5, 6,
            1, 2, 3,
        ]
    
        // you may want to show the "standard order" first,
        //  so pass a Bool parameter
    
        // shuffle the key order if specified
        let keyNums = shouldShuffle
            ? keyOrder.shuffled()
            : keyOrder
        
        // loop through and update the button titles
        // with the new order
    

    Here’s some updated code, using a "KeyPad" UIView subclass:

    enum PasscodeButtonType {
        case NUMBER, TOUCH, BACKSPACE
    }
    
    class PasscodeButton: UIButton {
        
        var pcButtonType: PasscodeButtonType = .NUMBER
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            setupButton()
        }
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setupButton()
        }
        
        override func awakeFromNib() {
            super.awakeFromNib()
            setupButton()
        }
        
        private func setupButton() {
            setTitleColor(UIColor.black, for: .normal)
            setTitleColor(UIColor.lightGray, for: .highlighted)
            layer.masksToBounds = true
            layer.borderColor = UIColor(red: 0/255.0, green: 0/255.0, blue: 0, alpha:1).cgColor
            layer.borderWidth = 2.0
            backgroundColor = .cyan
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            layer.cornerRadius = bounds.height * 0.5
            
            // button font and image sizes... adjust as desired
            let ptSize = bounds.height * 0.4
            titleLabel?.font = .systemFont(ofSize: ptSize)
            let config = UIImage.SymbolConfiguration(pointSize: ptSize)
            setPreferredSymbolConfiguration(config, forImageIn: [])
        }
        
    }
    
    class KeyPadView: UIView {
        
        // closures so we can tell the controller something happened
        var touchIDTapped: (()->())?
        var backSpaceTapped: (()->())?
        var numberTapped: ((String)->())?
        
        var spacing: CGFloat = 16
        
        private let outerStack = UIStackView()
    
        init(spacing spc: CGFloat) {
            self.spacing = spc
            super.init(frame: .zero)
            commonInit()
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
        
        func commonInit() -> Void {
            
            // load your TouchID and Backspace button images
            var touchImg: UIImage!
            var backImg: UIImage!
            
            if let img = UIImage(named: "myTouchImage") {
                touchImg = img
            } else {
                if #available(iOS 14.0, *) {
                    touchImg = UIImage(systemName: "touchid")
                } else if #available(iOS 13.0, *) {
                    touchImg = UIImage(systemName: "snow")
                } else {
                    fatalError("No TouchID button image available!")
                }
            }
            if let img = UIImage(named: "myBackImage") {
                backImg = img
            } else {
                if #available(iOS 13.0, *) {
                    backImg = UIImage(systemName: "delete.left.fill")
                } else {
                    fatalError("No BackSpace button image available!")
                }
            }
    
            outerStack.axis = .vertical
            outerStack.distribution = .fillEqually
            outerStack.spacing = spacing
            
            // add 3 "rows" of NUMBER buttons
            for _ in 1...3 {
                let hStack = UIStackView()
                hStack.distribution = .fillEqually
                hStack.spacing = outerStack.spacing
                for _ in 1...3 {
                    let btn = PasscodeButton()
                    // these are NUMBER buttons
                    btn.pcButtonType = .NUMBER
                    // square / round (1:1 ratio) buttons
                    //  for all buttons except the bottom "Zero" button
                    btn.heightAnchor.constraint(equalTo: btn.widthAnchor).isActive = true
                    btn.addTarget(self, action: #selector(keyButtonTapped(_:)), for: .touchUpInside)
                    hStack.addArrangedSubview(btn)
                }
                outerStack.addArrangedSubview(hStack)
            }
    
            // now add bottom row of TOUCH / 0 / BACKSPACE buttons
            let hStack = UIStackView()
            hStack.distribution = .fillEqually
            hStack.spacing = outerStack.spacing
            
            var btn: PasscodeButton!
            
            btn = PasscodeButton()
            btn.pcButtonType = .TOUCH
            btn.setImage(touchImg, for: [])
            btn.heightAnchor.constraint(equalTo: btn.widthAnchor).isActive = true
            btn.addTarget(self, action: #selector(keyButtonTapped(_:)), for: .touchUpInside)
            hStack.addArrangedSubview(btn)
    
            btn = PasscodeButton()
            btn.pcButtonType = .NUMBER
            btn.setTitle("0", for: [])
            btn.heightAnchor.constraint(equalTo: btn.widthAnchor).isActive = true
            btn.addTarget(self, action: #selector(keyButtonTapped(_:)), for: .touchUpInside)
            hStack.addArrangedSubview(btn)
            
            btn = PasscodeButton()
            btn.pcButtonType = .BACKSPACE
            btn.setImage(backImg, for: [])
            btn.heightAnchor.constraint(equalTo: btn.widthAnchor).isActive = true
            btn.addTarget(self, action: #selector(keyButtonTapped(_:)), for: .touchUpInside)
            hStack.addArrangedSubview(btn)
    
            // add bottom buttons row
            outerStack.addArrangedSubview(hStack)
            
            outerStack.translatesAutoresizingMaskIntoConstraints = false
            addSubview(outerStack)
            
            NSLayoutConstraint.activate([
                outerStack.topAnchor.constraint(equalTo: topAnchor, constant: spacing),
                outerStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: spacing),
                outerStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -spacing),
                outerStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -spacing),
            ])
    
            // use "standard number pad order" for the first time
            updateNumberKeys(shouldShuffle: false)
        }
        
        func updateNumberKeys(shouldShuffle b: Bool = true) -> Void {
    
            let keyOrder: [Int] = [
                7, 8, 9,
                4, 5, 6,
                1, 2, 3,
                0,
            ]
    
            // shuffle the key order if specified
            let keyNumbers = b == true
                ? keyOrder.shuffled()
                : keyOrder
            
            // index to step through array
            var numIDX: Int = 0
            
            // get first 3 rows of buttons
            let rows = outerStack.arrangedSubviews.prefix(3)
            
            // loop through buttons, changing their titles
            rows.forEach { v in
                guard let hStack = v as? UIStackView else {
                    fatalError("Bad Setup!")
                }
                hStack.arrangedSubviews.forEach { b in
                    guard let btn = b as? PasscodeButton else {
                        fatalError("Bad Setup!")
                    }
                    btn.setTitle("(keyNumbers[numIDX])", for: [])
                    numIDX += 1
                }
            }
    
            // change title of center button on bottom row
            guard let lastRowStack = outerStack.arrangedSubviews.last as? UIStackView,
                  lastRowStack.arrangedSubviews.count == 3,
                  let btn = lastRowStack.arrangedSubviews[1] as? PasscodeButton
            else {
                fatalError("Bad Setup!")
            }
            btn.setTitle("(keyNumbers[numIDX])", for: [])
    
        }
        
        @objc func keyButtonTapped(_ sender: Any?) -> Void {
            guard let btn = sender as? PasscodeButton else {
                return
            }
            
            switch btn.pcButtonType {
            case .TOUCH:
                // tell the controller TouchID was tapped
                touchIDTapped?()
            case .BACKSPACE:
                // tell the controller BackSpace was tapped
                backSpaceTapped?()
            default:
                guard let n = btn.currentTitle else {
                    // button has no title?
                    return
                }
                // tell the controller a NUmber Key was tapped
                numberTapped?(n)
            }
            
            // update the number keys, but shuffle them
            updateNumberKeys()
        }
        
    }
    
    class PassCodeViewController: UIViewController {
        
        var keyPad: KeyPadView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // play with these to see how the button sizes / spacing looks
            let keyPadSpacing: CGFloat = 12
            let keyPadWidth: CGFloat = 240
            
            // init with button spacing as desired
            keyPad = KeyPadView(spacing: keyPadSpacing)
            
            keyPad.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(keyPad)
            
            let g = view.safeAreaLayoutGuide
    
            // center keyPad view
            //  its height will be set by its layout
            NSLayoutConstraint.activate([
                keyPad.widthAnchor.constraint(equalToConstant: keyPadWidth),
                keyPad.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                keyPad.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            ])
            
            // let's show the frame of the keyPad
            keyPad.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            
            // set closures
            keyPad.numberTapped = { [weak self] str in
                guard let self = self else {
                    return
                }
                print("Number key tapped:", str)
                // do something with the number string
            }
            
            keyPad.touchIDTapped = { [weak self] in
                guard let self = self else {
                    return
                }
                print("TouchID was tapped!")
                // do something because TouchID button was tapped
            }
            
            keyPad.backSpaceTapped = { [weak self] in
                guard let self = self else {
                    return
                }
                print("BackSpace was tapped!")
                // do something because BackSpace button was tapped
            }
            
        }
        
    }
    

    and here’s how it looks, setting the keypad view width to 240 and the button spacing to 12:

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search