带 UIStackView 的密码屏幕,Swift

Passcode screen with UIStackView, Swift

我正在尝试实现密码屏幕,但我在对齐方面遇到了问题,如您在这张图片中所见。

我想做的是,每行有三个按钮,所以它实际上看起来像一个“键盘”。我不太确定我该怎么做。我考虑过在第一个垂直堆栈视图的内部制作,另外四个水平堆栈视图,但无法做到。任何建议或帮助将不胜感激。谢谢:)

代码如下。

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
  }
}

如果 PasscodeButtons 很重要,这里也有代码。

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
  }
}

按照以下步骤:-

  • 采用垂直堆栈视图并在其中添加三个按钮(第一行按钮)
  • 采用垂直堆栈视图并在其中添加三个按钮(用于第二行按钮)
  • 采用垂直堆栈视图并在其中添加三个按钮(用于第三行按钮)
  • 采用垂直堆栈视图并在其中添加三个按钮(用于第四行按钮)
  • 取一个水平堆栈视图并在其中添加所有这 4 个堆栈视图。

大意是:

  • 需要 4 个水平堆栈视图“按钮行”...3 行,每行 3 个按钮,加上 1 行,1 个按钮(“零”按钮)
  • 创建一个垂直堆栈视图来容纳按钮的“行”
  • 将所有堆栈视图分布设置为 .fillEqually
  • 将所有堆栈视图间距设置为相同的值

然后,要生成所有内容,请为键数字创建一个 Int 数组,布局像键盘一样:

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

遍历,创建每一行按钮。

这是一个简单的例子(我稍微修改了你的 PasscodeButton class):

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!")
    }
    
}

输出:

您可能想要调整尺寸,但这应该让您顺利进行。


编辑 - 评论“我希望 0 留在中间的最后一行,在左侧我会弹出触摸 ID 图标在右边的退格按钮上,我怎么能把最后一行从洗牌中去掉?"

当您创建按钮“网格”时:

  • 创建前三“行”,但将按钮标题留空。
  • 创建 3 个按钮的“底行”
    • 使用“touchID”图像设置第一个按钮
    • 将第二个按钮的标题设置为“0”
    • 用“backSpace”图像设置第三个按钮
  • 然后调用一个函数来设置“数字”按钮

keyNums 数组更改为:

    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

这是一些更新的代码,使用“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
        }
        
    }
    
}

这是它的外观,将键盘视图宽度设置为 240,将按钮间距设置为 12