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:
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)
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.
When I m clicking on button , selector is not getting called.
There are only two component is the cell , 1 is image and another is UIButton.
Below is the code for collection cell. is there any other way to add method.
class AttachmentCell: UICollectionViewCell {
weak var delegate: AttachmentCellDelegate?
let removeButton: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(named: "close_icon"), for: .normal)
button.addTarget(self, action: #selector(removeButtonTapped), for: .touchUpInside)
button.isUserInteractionEnabled = true
return button
}()
let imageView: UIImageView = {
let imgView = UIImageView()
imgView.contentMode = .scaleAspectFill
imgView.clipsToBounds = true
return imgView
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.red
self.isUserInteractionEnabled = true
self.contentView.addSubview(imageView)
imageView.isUserInteractionEnabled = true
self.contentView.addSubview(removeButton)
//self.addSubview(removeButton)
imageView.snp.makeConstraints { (make) in
make.leading.equalToSuperview()
make.trailing.equalToSuperview()
make.top.equalToSuperview()
make.bottom.equalToSuperview()
}
removeButton.snp.makeConstraints { (make) in
make.width.equalTo(40)
make.height.equalTo(40)
make.top.equalTo(imageView.snp.top).offset(-5)
make.trailing.equalTo(self.imageView.snp.trailing).offset(5)
}
self.backgroundColor = UIColor.gray
}
#objc func removeButtonTapped() {
delegate?.didRemoveButtonTapped()
}
}
Change let removeButton to lazy var removeButton.
self doesn't exist until init has been called. When you add a target to self in a let constant, you are defining it before init has been called.
Alternatively, just call addTarget in the init block.
I have a UIView class
class FloatingView : UIView {
lazy var floatingButton : UIButton = {
let button = UIButton(type: UIButtonType.system)
button.setBackgroundImage(#imageLiteral(resourceName: "ic_add_circle_white_36pt"), for: .normal)
button.tintColor = UIColor().themePurple()
button.addTarget(self, action: #selector(buttonClicked), for: UIControlEvents.touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews(){ addSubview(floatingButton) }
override func didMoveToWindow() {
super.didMoveToWindow()
floatingButton.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor).isActive = true
floatingButton.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor).isActive = true
floatingButton.widthAnchor.constraint(equalToConstant: 60).isActive = true
floatingButton.heightAnchor.constraint(equalToConstant: 60).isActive = true
}
#objc func buttonClicked (){
print("Clicked..")
}
Added this to view by
let floatingButton = FloatingView()
view.addSubview(floatingButton)
I've also specified the constraints for the floating view .
The button got added to view as expected but the "buttonClicked" function not invoked when the button is clicked . The fade animation on the button when clicked is working though.I've tried UITapGesture but not working .
I've update the class as below
class FloatingView{
lazy var floatingButton : UIButton = {
let button = UIButton(type: UIButtonType.system)
button.setBackgroundImage(#imageLiteral(resourceName: "ic_add_circle_white_36pt"), for: .normal)
button.tintColor = UIColor().themePurple()
button.addTarget(self, action: #selector(buttonClicked(_:)), for: UIControlEvents.touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var view : UIView!
func add(onview: UIView ){
view = onview
configureSubViews()
}
private func configureSubViews(){
view.addSubview(floatingButton)
floatingButton.rightAnchor.constraint(equalTo: view.layoutMarginsGuide.rightAnchor).isActive = true
floatingButton.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor).isActive = true
floatingButton.widthAnchor.constraint(equalToConstant: 60).isActive = true
floatingButton.heightAnchor.constraint(equalToConstant: 60).isActive = true
}
#objc func buttonClicked(_ sender : UIButton){
print("Button Clicked")
}
}
And in controller
let flButton = FloatingView()
flButton.add(onview: view)
I'm trying to create a floating action button like this. I'm not sure whether I'm doing it the right way.
try to change the action to
action: #selector(buttonClicked(_:))
and the function to
#objc func buttonClicked(_ sender: UIButton){..}
I somehow fixed it by setting the constrains from within the custom class to its superview rather than from the controller.
func configureSubviews(){
if let superView = superview {
superView.addSubview(floatingButton)
widthAnchor.constraint(equalToConstant: circleSpanArea).isActive = true
heightAnchor.constraint(equalTo: widthAnchor).isActive = true
centerXAnchor.constraint(equalTo: floatingButton.centerXAnchor).isActive = true
centerYAnchor.constraint(equalTo: floatingButton.centerYAnchor).isActive = true
floatingButton.rightAnchor.constraint(equalTo: superView.layoutMarginsGuide.rightAnchor).isActive = true
floatingButton.bottomAnchor.constraint(equalTo: superView.layoutMarginsGuide.bottomAnchor).isActive = true
floatingButton.heightAnchor.constraint(equalToConstant: 60).isActive = true
floatingButton.widthAnchor.constraint(equalToConstant: 60).isActive = true
}
}