简体   繁体   中英

Swift: UIButton not clickable, NSLayoutConstraint heightAnchor issues

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

}

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.

There are many benefits to using UIStackView s... 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:

在此处输入图像描述

在此处输入图像描述

在此处输入图像描述

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM