总体思路是:
- 需要 4 个水平堆栈视图“按钮行”... 3 行,每行 3 个按钮,再加上一行,1 个按钮(“零”按钮)
- 创建一个垂直堆栈视图来保存按钮的“行”
- 将所有堆栈视图分布设置为
.fillEqually
- 将所有堆栈视图间距设置为相同的值
然后,要生成所有内容,请为键号创建一个 Ints 数组,其布局类似于键盘:
let keyNums: [[Int]] = [
[7, 8, 9],
[4, 5, 6],
[1, 2, 3],
[0],
]
循环,创建每一行按钮。
这是一个简单的示例(我稍微修改了您的 PasscodeButton 类):
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”
- 使用“退格”图像设置第三个按钮
- 然后调用一个函数来设置“数字”按钮
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
子类的更新代码:
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
: