How to create a reusable button class with a stack view in UIKit? - ios

I'm using a class to build a reusable button (image below) that uses a stack view to position two labels vertically, and allows me to configure both labels' text when called.
I tried to add "label" and "subLabel" into a UIStackView in the init method below, but the stack isn't being added onto the button's view.
What would be the best way to integrate a stack view into this custom button class?
struct ActivityButtonVM {
let labelText: String
let subLabelText: String
let action: Selector
}
final class ActivityButton: UIButton {
private let label: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .black
return label
}()
private let subLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .gray
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setBackgroundImage(Image.setButtonBg, for: .normal)
let stack = UIStackView(arrangedSubviews: [label, subLabel])
stack.axis = .vertical
stack.alignment = .center
addSubview(stack)
clipsToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with viewModel: ActivityButtonVM) {
label.text = viewModel.labelText
subLabel.text = viewModel.subLabelText
self.addTarget(SetActivityVC(), action: viewModel.action,
for: .touchUpInside)
}
}
This is how I'm using this custom button class:
class SetActivityVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
lazy var firstButton: UIButton = {
let button = ActivityButton()
button.configure(with: ActivityButtonVM(labelText: "No Exercise", subLabelText: "no exercise or very infrequent", action: #selector(didTapFirst))
return button
}()
lazy var secondButton: UIButton = {
let button = ActivityButton()
button.configure(with: ActivityButtonVM(labelText: "Light Exercise", subLabelText: "some light cardio/weights a few times per week", action: #selector(didTapSecond))
return button
}()
#objc func didTapFirst() {
print("Tapped 1")
}
#objc func didTapSecond() {
print("Tapped 2")
}
}
extension SetActivityVC {
fileprivate func setupViews() {
addViews()
constrainViews()
}
fileprivate func addViews() {
view.addSubview(firstButton)
view.addSubview(secondButton)
}
fileprivate func constrainViews() {
firstButton.centerXToSuperview()
secondButton.centerXToSuperview()
secondButton.topToBottom(of: firstButton, offset: screenHeight * 0.03)
}
}

First, you are not calling your init(frame:) when initialising your buttons:
let button = ActivityButton()
You are just calling the initialiser you inherited from NSObject, so of course the stack views are not added.
You can add a parameterless convenience initialiser yourself, that calls self.init(frame:):
convenience init() {
self.init(frame: .zero)
}
and then the stack views will be added.
I think you would also need to add:
stack.translatesAutoresizingMaskIntoConstraints = false
to stop the autoresizing mask constraints from causing the stack view to have a .zero frame.
Additionally, you should add constraints to the stack view so that it is positioned correctly with respect to the button. (probably pin the 4 sides to the button's 4 sides?)
Last but not least, the way that you are adding the target is incorrect. You are adding a new instance of SetActivityVC as the target here, rather than the instance of the VC that has the button.
self.addTarget(SetActivityVC(), action: viewModel.action,
for: .touchUpInside)
Instead, if you want to do this with target-action pairs, you should include the target in the view model as well:
struct ActivityButtonVM {
let labelText: String
let subLabelText: String
let target: Any // <----
let action: Selector
}
...
self.addTarget(viewModel.target, action: viewModel.action,
for: .touchUpInside)
Tip: rather than using colours such as .black and .gray, use .label and .secondaryLabel so that it also looks good in dark mode.

You can use alternative way: new UIButton.configuration, declare your buttons:
let myButton1 = UIButton()
let myButton2 = UIButton()
let myButton3 = UIButton()
let myButton4 = UIButton()
now add this extension for button configuration:
extension UIViewController {
func buttonConfiguration(button: UIButton, config: UIButton.Configuration, title: String, subtitle: String, bgColor: UIColor, foregColor: UIColor, imageSystemName: String, imageTintColor: UIColor) {
let b = button
b.configuration = config
b.configuration?.title = title
b.configuration?.titleAlignment = .center
b.configuration?.subtitle = subtitle
b.configuration?.baseForegroundColor = foregColor
b.configuration?.baseBackgroundColor = bgColor
b.configuration?.image = UIImage(systemName: imageSystemName)?.withTintColor(imageTintColor, renderingMode: .alwaysOriginal)
b.configuration?.imagePlacement = .top
b.configuration?.imagePadding = 6
b.configuration?.cornerStyle = .large
}
how to use, in viewDidLoad set your buttons and relative targets:
buttonConfiguration(button: myButton1, config: .filled(), title: "My Button One", subtitle: "This is first button", bgColor: colorUpGradient, foregColor: .white, imageSystemName: "sun.min", imageTintColor: .orange)
myButton1.addTarget(self, action: #selector(didTapFirst), for: .touchUpInside)
buttonConfiguration(button: myButton2, config: .filled(), title: "My Button Two", subtitle: "This is second button", bgColor: .fuxiaRed, foregColor: .white, imageSystemName: "cloud", imageTintColor: .white)
myButton2.addTarget(self, action: #selector(didTapSecond), for: .touchUpInside)
buttonConfiguration(button: myButton3, config: .filled(), title: "My Button Tree", subtitle: "This is third button", bgColor: .celesteCiopChiaro, foregColor: .black, imageSystemName: "cloud.drizzle", imageTintColor: .red)
myButton3.addTarget(self, action: #selector(didTapThird), for: .touchUpInside)
buttonConfiguration(button: myButton4, config: .filled(), title: "My Button Four", subtitle: "This is four button", bgColor: .darkYellow, foregColor: .black, imageSystemName: "cloud.bolt", imageTintColor: .black)
myButton4.addTarget(self, action: #selector(didTapFour), for: .touchUpInside)
set your stackView and constraints:
let stackView = UIStackView(arrangedSubviews: [myButton1, myButton2, myButton3, myButton4])
stackView.axis = .vertical
stackView.spacing = 12
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stackView.heightAnchor.constraint(equalToConstant: 372).isActive = true // 84(height of single button) * 4(number of buttons) = 336 + 36(total stackView spaces from buttons) = 372(height of intere stackView)
stackView.widthAnchor.constraint(equalToConstant: view.frame.width - 60).isActive = true // set width of button
add buttons functions:
#objc func didTapFirst() {
print("Tapped 1")
}
#objc func didTapSecond() {
print("Tapped 2")
}
#objc func didTapThird() {
print("Tapped 3")
}
#objc func didTapFour() {
print("Tapped 4")
}
This is the result:

Related

What is best practice for creating a reusable custom button views?

I have three buttons below that have the same UI, the only differences are the text for the labels and tap gesture actions. It looks like this:
What is the best practice for creating a reusable custom button view based on this situation?
So far I tried using: (1) custom button class but had difficulty implementing a stack view where I can configure the two labels in the button, (2) UIButton extension but an issue where tapping the button caused the app to crash
class SetActivityVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
lazy var firstButton: UIButton = {
let button = UIButton()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapFirst))
button.addGestureRecognizer(tapGesture)
button.setBackgroundImage(Image.setButtonBg, for: .normal)
button.addShadowEffect()
let label = UILabel()
label.text = "No Exercise"
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
label.textColor = .black
let subLabel = UILabel()
subLabel.text = "no exercise or very infrequent"
subLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
subLabel.textColor = .gray
let stack = UIStackView(arrangedSubviews: [label, subLabel])
stack.axis = .vertical
stack.alignment = .center
stack.isUserInteractionEnabled = true
stack.addGestureRecognizer(tapGesture)
button.addSubview(stack)
stack.centerInSuperview()
return button
}()
lazy var secondButton: UIButton = {
let button = UIButton()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapSecond))
button.addGestureRecognizer(tapGesture)
button.setBackgroundImage(Image.setButtonBg, for: .normal)
button.addTarget(self, action: #selector(didTapSecond), for: .touchUpInside)
button.addShadowEffect()
let label = UILabel()
label.text = "Light Exercise"
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
label.textColor = .black
let subLabel = UILabel()
subLabel.text = "some light cardio/weights a few times per week"
subLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
subLabel.textColor = .gray
let stack = UIStackView(arrangedSubviews: [label, subLabel])
stack.axis = .vertical
stack.alignment = .center
button.addSubview(stack)
stack.centerInSuperview()
return button
}()
lazy var thirdButton: UIButton = {
let button = UIButton()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapThird))
button.addGestureRecognizer(tapGesture)
button.setBackgroundImage(Image.setButtonBg, for: .normal)
button.addTarget(self, action: #selector(didTapSecond), for: .touchUpInside)
button.addShadowEffect()
let label = UILabel()
label.text = "Moderate Exercise"
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
label.textColor = .black
let subLabel = UILabel()
subLabel.text = "lifting/cardio regularly but not super intense"
subLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
subLabel.textColor = .gray
let stack = UIStackView(arrangedSubviews: [label, subLabel])
stack.axis = .vertical
stack.alignment = .center
button.addSubview(stack)
stack.centerInSuperview()
return button
}()
#objc func didTapFirst() {
print("Tapped 1")
}
#objc func didTapSecond() {
print("Tapped 2")
}
#objc func didTapThird() {
print("Tapped 3")
}
}
extension SetActivityVC {
fileprivate func setupViews() {
addViews()
constrainViews()
}
fileprivate func addViews() {
view.addSubview(firstButton)
view.addSubview(secondButton)
view.addSubview(thirdButton)
}
// Using TinyConstraints
fileprivate func constrainViews() {
firstButton.centerXToSuperview()
secondButton.centerXToSuperview()
secondButton.topToBottom(of: firstButton, offset: screenHeight * 0.03)
thirdButton.centerXToSuperview()
thirdButton.topToBottom(of: secondButton, offset: screenHeight * 0.03)
}
}
There's no universal answer because each situation is unique, but generally there are several common patterns:
Implement a factory method that would create a button, set up all its properties and return it.
Subclass UIButton and add new behavior and reasonable defaults.
Subclass UIControl for something totally custom, like a control that is composed out several other views.
Now, your particular problem seems to be implementing a reusable button with two differently styled lines of text inside.
Adding labels as subviews to UIButton is something I definitely wouldn't recommend. This breaks accessiblity and you'll have to do a lot of work to support different button states like highlighted or disabled.
Instead, I highly recommend to make use of a great feature of UIButton: it supports attributed strings for title, and titles can be multiline as well because you have access to the button's titleLabel property.
Subclassing UIButton just for reasonable defaults and ease of setup seems like a good choice here:
struct TwoLineButtonModel {
let title: String
let subtitle: String
let action: () -> Void
}
final class TwoLineButton: UIButton {
private var action: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
addTarget(self, action: #selector(handleTap(_:)), for: .touchUpInside)
setUpAppearance()
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with model: TwoLineButtonModel) {
[.normal, .highlighted, .disabled].forEach {
setAttributedTitle(
makeButtonTitle(
title: model.title,
subtitle: model.subtitle,
forState: $0
),
for: $0
)
}
action = model.action
}
#objc private func handleTap(_ sender: Any) {
action?()
}
private func setUpAppearance() {
backgroundColor = .yellow
layer.cornerRadius = 16
titleLabel?.numberOfLines = 0
contentEdgeInsets = UIEdgeInsets(top: 16, left: 8, bottom: 16, right: 8)
}
private func makeButtonTitle(
title: String,
subtitle: String,
forState state: UIControl.State
) -> NSAttributedString {
let centeredParagraphStyle = NSMutableParagraphStyle()
centeredParagraphStyle.alignment = .center
let primaryColor: UIColor = {
switch state {
case .highlighted:
return .label.withAlphaComponent(0.5)
case .disabled:
return .label.withAlphaComponent(0.3)
default:
return .label
}
}()
let secondaryColor: UIColor = {
switch state {
case .highlighted:
return .secondaryLabel.withAlphaComponent(0.3)
case .disabled:
return .secondaryLabel.withAlphaComponent(0.1)
default:
return .secondaryLabel
}
}()
let parts = [
NSAttributedString(
string: title + "\n",
attributes: [
.font: UIFont.preferredFont(forTextStyle: .title1),
.foregroundColor: primaryColor,
.paragraphStyle: centeredParagraphStyle
]
),
NSAttributedString(
string: subtitle,
attributes: [
.font: UIFont.preferredFont(forTextStyle: .body),
.foregroundColor: secondaryColor,
.paragraphStyle: centeredParagraphStyle
]
)
]
let string = NSMutableAttributedString()
parts.forEach { string.append($0) }
return string
}
}
The text styles and colors in my example may not exactly match what you need, but it's easily adjustable and you can take it from here. Move things that should be customizable into the view model while keeping the reasonable defaults as a private implementation. Look into tutorials on NSAttributedString if you're not yet familiar with it, it gives you a lot of freedom in styling texts.

Passcode screen with UIStackView, Swift

I am trying to implement passcode screen, but I am having trouble with alignment, as you can see in this picture.
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
}
}
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:
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:
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.

Button in UIStackView not clickable

I'm trying to add a button to my stack view. The button has a buttonTapped method that should be called when it is tapped. The problem is it is never being called, the button does not seem to be clickable.
class CustomButton: UIViewController {
var buttonDelegate: ButtonDelegate?
let button = UIButton(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width - 40, height: 30))
init(label: String) {
button.setTitle(label, for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .systemBlue
super.init(nibName: nil, bundle: nil)
}
#objc func buttonTapped() {
print("this never gets printed")
buttonDelegate?.buttonTapped(buttonType: .submit)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
view.addSubview(button)
}
}
And then my main view controller:
protocol ButtonDelegate {
func buttonTapped(buttonType: ButtonType)
}
class DynamicViewController: UIViewController, ButtonDelegate {
lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
lazy var contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private func setupViews() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(stackView)
let btn = CustomButton(label: "hi")
btn.buttonDelegate = self
self.stackView.addArrangedSubview(btn.view)
}
func buttonTapped(buttonType: ButtonType) {
print("also never gets printed")
}
}
There is nothing overlapping the button or anything like that:
My question is why the button is not clickable.
You are adding the view controller as a subview. So you also need to add as a child.
Add bellow code after self.stackView.addArrangedSubview(btn.view) this line.
self.addChild(btn)
btn.didMove(toParent: self)

How to hide the letters on a .numberPad keyboard?

By default, when using a .numberPad keyboard on iPhone, the number keys also feature letters at the bottom of each key.
The letters are purely cosmetic, to help people with number input; but in my case (entering item quantities), they’re completely superfluous, maybe even confusing.
Is it possible to configure the keyboard to hide these letters?
Should I implement my own keyboard view just to properly present the keys?
I don’t believe there is currently a “digit only” keyboard without the text characters.
But you can create your own:
textField.inputView = NumericKeyboard(target: textField)
Where
class DigitButton: UIButton {
var digit: Int = 0
}
class NumericKeyboard: UIView {
weak var target: UIKeyInput?
var numericButtons: [DigitButton] = (0...9).map {
let button = DigitButton(type: .system)
button.digit = $0
button.setTitle("\($0)", for: .normal)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.addTarget(self, action: #selector(didTapDigitButton(_:)), for: .touchUpInside)
return button
}
var deleteButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("⌫", for: .normal)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = "Delete"
button.addTarget(self, action: #selector(didTapDeleteButton(_:)), for: .touchUpInside)
return button
}()
init(target: UIKeyInput) {
self.target = target
super.init(frame: .zero)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Actions
extension NumericKeyboard {
#objc func didTapDigitButton(_ sender: DigitButton) {
target?.insertText("\(sender.digit)")
}
#objc func didTapDeleteButton(_ sender: DigitButton) {
target?.deleteBackward()
}
}
// MARK: - Private initial configuration methods
private extension NumericKeyboard {
func configure() {
autoresizingMask = [.flexibleWidth, .flexibleHeight]
addButtons()
}
func addButtons() {
let stackView = createStackView(axis: .vertical)
stackView.frame = bounds
stackView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(stackView)
for row in 0 ..< 3 {
let subStackView = createStackView(axis: .horizontal)
stackView.addArrangedSubview(subStackView)
for column in 0 ..< 3 {
subStackView.addArrangedSubview(numericButtons[row * 3 + column + 1])
}
}
let subStackView = createStackView(axis: .horizontal)
stackView.addArrangedSubview(subStackView)
let blank = UIView()
blank.layer.borderWidth = 0.5
blank.layer.borderColor = UIColor.darkGray.cgColor
subStackView.addArrangedSubview(blank)
subStackView.addArrangedSubview(numericButtons[0])
subStackView.addArrangedSubview(deleteButton)
}
func createStackView(axis: NSLayoutConstraint.Axis) -> UIStackView {
let stackView = UIStackView()
stackView.axis = axis
stackView.alignment = .fill
stackView.distribution = .fillEqually
return stackView
}
}
That yields:
Clearly, you can go nuts, customizing your keyboard to look however you’d like to. The above is fairly primitive, something I just dashed together. But it illustrates the idea: Make you own input view and use the UIKeyInput protocol to communicate keyboard input to the control.

Action not being called when button is tapped in a stack view

I have a custom view that includes a stack view. Inside the stack view I have a label and a button.
I created my stack view, label and button in the following way and added them to the parent view.
class HomeView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
stackView.addArrangedSubview(haveAccount)
stackView.addArrangedSubview(signin)
stackView.setCustomSpacing(4.0, after: haveAccount)
}
let stackView: UIStackView = {
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.distribution = .fillProportionally
stack.alignment = .fill
stack.isUserInteractionEnabled = false
return stack
}()
let haveAccount: UILabel = {
let label = UILabel()
...
return label
}()
let signin: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Sign in", for: .normal)
button.titleLabel?.font = UIFont(name: "Avenir", size: 14)
button.setTitleColor(UIColor.white, for: .normal)
button.addTarget(self, action: #selector(HomeController.loginClicked(_:)), for: .touchUpInside)
return button
}()
}
In my view controller I add the view to the controller's base view and set the constraints. I also create the method that should be called when the signin button is tapped.
override func viewDidLoad() {
super.viewDidLoad()
homeView = HomeView()
homeView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(homeView)
homeView.fullscreenView(parentView: view)
}
#objc func loginClicked(_ sender: UIButton) {
print("sign in button pressed")
}
When I press the button the loginClicked method is not called. Now I did tried moving the loginClicked method to the custom view and changing the addTarget accordingly and loginClicked method is called. This being said I know the button is clickable but I don't think the target for the button action is correct and that is why the loginClicked method in the view controller is not being called.
You can use Protocol/Delegation
//1. Create a protocol
protocol HomeViewDelegate{
func loginButtonClicked(sender: UIButton)
}
class HomeView: UIView {
//2. Create a delegate
var delegate: HomeViewDelegate?
let stackView: UIStackView = {
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.distribution = .fillProportionally
stack.alignment = .fill
stack.isUserInteractionEnabled = false
return stack
}()
let haveAccount: UILabel = {
let label = UILabel()
return label
}()
let signin: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Sign in", for: .normal)
button.titleLabel?.font = UIFont(name: "Avenir", size: 14)
button.setTitleColor(UIColor.white, for: .normal)
button.addTarget(self, action: #selector(loginClicked(sender:)), for: .touchUpInside)
button.backgroundColor = .red
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
stackView.addArrangedSubview(haveAccount)
stackView.addArrangedSubview(signin)
stackView.setCustomSpacing(4.0, after: haveAccount)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//3. Call your protocol method via delegate
#objc func loginClicked(sender: UIButton) {
if let delegate = delegate{
delegate.loginButtonClicked(sender: sender)
}
}
}
In You Caller ViewController create an extension
extension ViewController: HomeViewDelegate{
func loginButtonClicked(sender: UIButton) {
print("login Button Clicked")
}
}
First of all you set userInteractionEnabled property of your stackView to false, set it to true. Then if it does not work consider the following approach:
There are two possible ways to fix this, first is adding the target from ViewController, and the other one is using delegation.
I think the first way would be easier to implement for you.
You need to add your target from your ViewController class.
First update your view class and get rid of adding a target:
class HomeView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
stackView.addArrangedSubview(haveAccount)
stackView.addArrangedSubview(signin)
stackView.setCustomSpacing(4.0, after: haveAccount)
}
let stackView: UIStackView = {
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.distribution = .fillProportionally
stack.alignment = .fill
stack.isUserInteractionEnabled = true
return stack
}()
let haveAccount: UILabel = {
let label = UILabel()
...
return label
}()
let signin: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Sign in", for: .normal)
button.titleLabel?.font = UIFont(name: "Avenir", size: 14)
button.setTitleColor(UIColor.white, for: .normal)
return button
}()
}
Now in your ViewController:
override func viewDidLoad() {
super.viewDidLoad()
homeView = HomeView()
homeView.translatesAutoresizingMaskIntoConstraints = false
homeView.signIn.addTarget(self, action: #selector(loginClicked), for: .touchUpInside)
view.addSubview(homeView)
homeView.fullscreenView(parentView: view)
}
#objc func loginClicked(_ sender: UIButton) {
print("sign in button pressed")
}
You need to add the right constraints, I had this problem, I had this problem and the solution was this.
Solution:
import Foundation
import UIKit
protocol HomeViewDelegate:class{
func loginButtonClicked(sender: UIButton)
}
class HomeView: UIView {
//2. Create a delegate
weak var delegate: HomeViewDelegate?
var stackView: UIStackView = {
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.distribution = .fillProportionally
stack.alignment = .fill
stack.axis = .vertical
stack.isUserInteractionEnabled = true
return stack
}()
let haveAccount: UILabel = {
let label = UILabel()
label.backgroundColor = .gray
label.text = "Label"
label.textAlignment = .center
return label
}()
let signin: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Sign in", for: .normal)
button.titleLabel?.font = UIFont(name: "Avenir", size: 14)
button.setTitleColor(UIColor.white, for: .normal)
button.addTarget(self, action: #selector(loginClicked(sender:)), for: .touchUpInside)
button.isUserInteractionEnabled = true
button.backgroundColor = .red
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
stackView.addArrangedSubview(haveAccount)
stackView.addArrangedSubview(signin)
stackView.setCustomSpacing(4.0, after: haveAccount)
addSubview(stackView)
NSLayoutConstraint.activate([
self.stackView.topAnchor.constraint(equalTo: self.topAnchor),
self.stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
self.stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
self.stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//3. Call your protocol method via delegate
#objc func loginClicked(sender: UIButton) {
if let delegate = delegate{
delegate.loginButtonClicked(sender: sender)
}
}
}
ViewController
override func viewDidLoad() {
super.viewDidLoad()
let homeView = HomeView()
homeView.translatesAutoresizingMaskIntoConstraints = false
homeView.delegate = self
self.view.addSubview(homeView)
NSLayoutConstraint.activate([
homeView.centerXAnchor.constraint(equalTo: centerXAnchor),
homeView.centerYAnchor.constraint(equalTo: centerYAnchor),
homeView.heightAnchor.constraint(equalToConstant: 300),
homeView.widthAnchor.constraint(equalToConstant: 300)
])
}
#objc func loginClicked(_ sender: UIButton) {
print("sign in button pressed")
}
add this line to your button code
button.isUserInteractionEnabled = true
re-active isUserInteractionEnabled again
let signin: UIButton = {
let button = UIButton()
button.backgroundColor = .blue
button.setTitle("Sign in", for: .normal)
button.titleLabel?.font = UIFont(name: "Avenir", size: 14)
button.setTitleColor(UIColor.white, for: .normal)
button.addTarget(self, action: #selector(ViewController.loginClicked(_:)), for: .touchUpInside)
button.isUserInteractionEnabled = true
return button
}()

Resources