1

I am Building a simple hangman game. I have built a simple keyboard out of UIButtons. The keyboard is inside a subview and each row is a seperate subview.

The Buttons are not clickable, I can get the top row working but then the other rows get pushed apart.

I have tried setting the NSLayoutConstraint height anchors and it will push the UIButtons out of their corresponding Views.

class ViewController: UIViewController {

// letterGuess
// usedLetters
// score/lives

var scoreLabel: UILabel!
var answerLabel: UILabel!
var characterButtons = [UIButton]()

var score = 0 {
    didSet {
        scoreLabel.text = "Score: \(score)"
    }
}
override func loadView() {
    view = UIView()
    view.backgroundColor = .white
    
    scoreLabel = UILabel()
    scoreLabel.translatesAutoresizingMaskIntoConstraints = false
    scoreLabel.textAlignment = .right
    scoreLabel.font = UIFont.systemFont(ofSize: 24)
    scoreLabel.text = "Score: 0"
    view.addSubview(scoreLabel)
    
    answerLabel = UILabel()
    answerLabel.translatesAutoresizingMaskIntoConstraints = false
    answerLabel.font = UIFont.systemFont(ofSize: 24)
    answerLabel.text = "ANSWER"
    answerLabel.numberOfLines = 1
    answerLabel.textAlignment = .center
    answerLabel.setContentHuggingPriority(UILayoutPriority(1), for: .vertical)
    view.addSubview(answerLabel)
    
    let buttonsView = UIView()
    buttonsView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(buttonsView)
    
    let row1View = UIView()
    row1View.translatesAutoresizingMaskIntoConstraints = false
    buttonsView.addSubview(row1View)
    
    let row2View = UIView()
    row2View.translatesAutoresizingMaskIntoConstraints = false
    row2View.setContentHuggingPriority(.defaultLow, for: .vertical)
    buttonsView.addSubview(row2View)
    
    let row3View = UIView()
    row3View.translatesAutoresizingMaskIntoConstraints = false
    buttonsView.addSubview(row3View)

    
    NSLayoutConstraint.activate([
        scoreLabel.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
        scoreLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor, constant: 0),
        
        answerLabel.topAnchor.constraint(equalTo: scoreLabel.bottomAnchor, constant: 25),
        answerLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        
        buttonsView.widthAnchor.constraint(equalToConstant: 1000),
        buttonsView.heightAnchor.constraint(equalToConstant: 300),
        buttonsView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        buttonsView.topAnchor.constraint(equalTo: answerLabel.bottomAnchor, constant: 20),
        buttonsView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: -20),
        
        row1View.leftAnchor.constraint(equalTo: buttonsView.leftAnchor),
        row1View.topAnchor.constraint(equalTo: buttonsView.topAnchor),
        row1View.widthAnchor.constraint(equalTo: buttonsView.widthAnchor),
        //row1View.heightAnchor.constraint(equalTo: buttonsView.heightAnchor, multiplier: 0.333, constant: 0),
        //row1View.heightAnchor.constraint(equalToConstant: 100),
        
        
        row2View.leftAnchor.constraint(equalTo: buttonsView.leftAnchor),
        row2View.topAnchor.constraint(equalTo: row1View.bottomAnchor),
        row2View.widthAnchor.constraint(equalTo: buttonsView.widthAnchor),
        //row2View.heightAnchor.constraint(equalTo: buttonsView.heightAnchor, multiplier: 0.333, constant: 0),
        //row2View.heightAnchor.constraint(equalToConstant: 100),
        
        row3View.leftAnchor.constraint(equalTo: buttonsView.leftAnchor),
        row3View.topAnchor.constraint(equalTo: row2View.bottomAnchor),
        row3View.widthAnchor.constraint(equalTo: buttonsView.widthAnchor),
        //row3View.heightAnchor.constraint(equalTo: buttonsView.heightAnchor, multiplier: 0.333, constant: 0),
        //row3View.heightAnchor.constraint(equalToConstant: 100),
       
        
        
    
    ])
    
    let width = 100
    let height = 100
    var i = 10
    
    for row in 0..<3 {
        print(row)
        switch row {
        case 0:
            i = 10
        case 1:
            i = 9
        case 2:
            i = 7
        default:
            return
        }
        for col in 0..<i {
            let characterButton = UIButton(type: .system)
            characterButton.titleLabel?.font = UIFont.systemFont(ofSize: 36)
            
            characterButton.layer.borderWidth = 1
            characterButton.layer.borderColor = UIColor.lightGray.cgColor
            characterButton.layer.backgroundColor = UIColor.white.cgColor
            characterButton.setTitle("#", for: .normal)

            let frame = CGRect(x: col * width, y: row * height, width: width, height: height)
            characterButton.frame = frame
            
            switch row {
            case 0:
                print(row)
                print("row 1")
                row1View.addSubview(characterButton)
            case 1:
                print(row)
                print("row 2")
                row2View.addSubview(characterButton)
            case 2:
                print(row)
                print("row 3")
                row3View.addSubview(characterButton)
            default:
                print("defualt")
                return
            }
            
            characterButtons.append(characterButton)
            
            
            characterButton.addTarget(self, action: #selector(characterTapped), for: .touchUpInside)
        }
    }
    
   buttonsView.backgroundColor = .purple
    row1View.backgroundColor = .red
    row2View.backgroundColor = .yellow
    row3View.backgroundColor = .green

}
1
  • why you are not using StackView Commented Jul 11, 2020 at 15:04

2 Answers 2

0

You have a bug in the place where you calculate the frame of the buttons to be placed in each row.

// your code
let frame = CGRect(x: col * width, y: row * height, width: width, height: height)

You don't need to change the y position of the button. It can just be 0 here since each row is within its own view.

// corrected code
let frame = CGRect(x: col * width, y: 0, width: width, height: height)

You should also set a height constraint for each row you have. All the buttons which were added were out of bounds of the parent view. This becomes visible when rowView.clipsToBounds = true is set. That's why your buttons weren't working.

I believe there is an issue in the loop as well running more than it needs to, but I haven't checked it.

Solution to your issue: I tried fixing your sample code and it works. Check here

Also try using a collection or stack view to solve the problem when you find time.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the help, I am very new to swift and UIKit, why is a collection or stack view the better option?
0

There are many benefits to using UIStackViews ... primarily the fact that they can be used to automatically arrange and size the subviews, making it easy to adapt your layout to different devices and screen-sizes.

Here is an example of your code, modified to use stack views to hold the buttons (I also added a custom CharacterButton class that will automatically set the button label's font size in a user-defined range / proportion):

class CharacterButton: UIButton {
    
    // this will automatically set the font size for the button
    // if the button width >= 100, font size will be maxSize
    // if it's less than 100, font size will be proportional
    // with a minimum font size of 20
    
    // these are declared as "var" so they can be changed at run-time if desired
    var maxSize: CGFloat = 36
    var minSize: CGFloat = 20
    var forWidth: CGFloat = 100
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    func commonInit() -> Void {
        
        // set title colors
        setTitleColor(.blue, for: .normal)
        setTitleColor(.lightGray, for: .highlighted)
        
        // maybe change title color when disabled?
        //setTitleColor(.darkGray, for: .disabled)
        
        // give it a border
        layer.borderWidth = 1
        layer.borderColor = UIColor.lightGray.cgColor
        layer.backgroundColor = UIColor.white.cgColor
        
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        let fSize = min(max(minSize, bounds.size.width / forWidth * maxSize), maxSize)
        titleLabel?.font = UIFont.systemFont(ofSize: fSize)
    }
    
}

class HangManViewController: UIViewController {
    
    // letterGuess
    // usedLetters
    // score/lives
    
    var scoreLabel: UILabel!
    var answerLabel: UILabel!
    var characterButtons = [UIButton]()
    
    var score = 0 {
        didSet {
            scoreLabel.text = "Score: \(score)"
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white
        
        // create and add the Score label
        scoreLabel = UILabel()
        scoreLabel.translatesAutoresizingMaskIntoConstraints = false
        scoreLabel.textAlignment = .right
        scoreLabel.font = UIFont.systemFont(ofSize: 24)
        scoreLabel.text = "Score: 0"
        view.addSubview(scoreLabel)
        
        // create and add the Answer label
        answerLabel = UILabel()
        answerLabel.translatesAutoresizingMaskIntoConstraints = false
        answerLabel.font = UIFont.systemFont(ofSize: 24)
        answerLabel.text = "ANSWER"
        answerLabel.numberOfLines = 1
        answerLabel.textAlignment = .center
        answerLabel.setContentHuggingPriority(UILayoutPriority(1), for: .vertical)
        view.addSubview(answerLabel)
        
        // create a view to hold the buttons
        let buttonsView = UIView()
        buttonsView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(buttonsView)
        
        // create a vertical "outer" stack view
        let outerStack = UIStackView()
        outerStack.axis = .vertical
        outerStack.distribution = .fillEqually

        // add it to the buttons holder view
        outerStack.translatesAutoresizingMaskIntoConstraints = false
        buttonsView.addSubview(outerStack)
        
        // create three "row" stack views
        let row1Stack = UIStackView()
        row1Stack.axis = .horizontal
        row1Stack.distribution = .fillEqually
        
        let row2Stack = UIStackView()
        row2Stack.axis = .horizontal
        row2Stack.distribution = .fillEqually
        
        let row3Stack = UIStackView()
        row3Stack.axis = .horizontal
        row3Stack.distribution = .fillEqually
        
        // add the 3 "row" stack views to the "outer" stack view
        [row1Stack, row2Stack, row3Stack].forEach {
            outerStack.addArrangedSubview($0)
        }
        
        let g = view.layoutMarginsGuide
        
        NSLayoutConstraint.activate([
            
            // constrain Score label to top-right
            scoreLabel.topAnchor.constraint(equalTo: g.topAnchor),
            scoreLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0),
            
            // constrain Answer label centered horizontally
            answerLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            // and 4-pts above the grid of buttons
            answerLabel.bottomAnchor.constraint(equalTo: buttonsView.topAnchor, constant: -4),

            // constrain buttons holder view Leading / Trailing
            buttonsView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            buttonsView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),

            // constrain buttons holder view Bottom with 20-pts "padding"
            buttonsView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20),
            
            // constrain all 4 sides of "outer" stack view to buttons holder view
            outerStack.topAnchor.constraint(equalTo: buttonsView.topAnchor),
            outerStack.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor),
            outerStack.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor),
            outerStack.bottomAnchor.constraint(equalTo: buttonsView.bottomAnchor),
            
        ])
        
        let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".map { String($0) }

        var j = 0
        
        // loop through the 3 "rows" adding 10, 9 and 7 buttons
        
        for (thisRow, numButtonsInThisRow) in zip([row1Stack, row2Stack, row3Stack], [10, 9, 7]) {

            for i in 0..<10 {
                
                if i < numButtonsInThisRow {
                    
                    // create a button
                    let characterButton = CharacterButton()
                    
                    // set its title
                    characterButton.setTitle(letters[j], for: .normal)

                    // maybe set button title to "#" when disabled?
                    //characterButton.setTitle("#", for: .disabled)

                    // give button a touchUp target
                    characterButton.addTarget(self, action: #selector(self.characterTapped(_:)), for: .touchUpInside)
                    
                    // add button to current row stack view
                    thisRow.addArrangedSubview(characterButton)
                    
                    // add button to characterButtons Array
                    characterButtons.append(characterButton)

                    // increment j
                    j += 1
                    
                } else {
                    
                    // we're past the number of character buttons that should be on this row
                    // so "fill it out" with bordered views
                    let v = UIView()
                    v.layer.borderWidth = 1
                    v.layer.borderColor = UIColor.lightGray.cgColor
                    v.layer.backgroundColor = UIColor.white.cgColor
                    thisRow.addArrangedSubview(v)
                    
                }
            }

        }
        
        // we want square buttons, so
        //  we only need to set the first button to have a 1:1 height:width ratio
        //  the stack views' fillEqually distribution will handle the rest
        if let v = characterButtons.first {
            v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
        }

        // just so we can see the frame of the answer label
        answerLabel.backgroundColor = .green
        
    }
    
    @objc func characterTapped(_ sender: UIButton) {
        // character button tapped
        
        // get its title
        let s = sender.currentTitle ?? "no title"
        
        // do we want to disable it?
        //sender.isEnabled = false

        // for now, print the Letter to the debug console
        print("Button \(s) was tapped!")

    }
    
}

Results:

enter image description here

enter image description here

enter image description here

2 Comments

Thanks for the example on UIStavkViews! I am confused with this line: for (thisRow, numButtonsInThisRow) in zip([row1Stack, row2Stack, row3Stack], [10, 9, 7]) { what is a zip function?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.