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.
Related
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.
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:
First off, I will have you know that I have looked at many many different posts on StackOverflow and on Google & Github. I have scoured everywhere for ANYTHING that will help me. But, nothing seems to work. Either it's out of date (by 10+ years) or written entirely in Objective-C. I know that there are other posts out there about this topic, but I need a solution in Swift 5.0 +, not an ancient one from 11 years ago in Obj-C that is totally deprecated in everything today.
Now, my question. I need to develop a keyboard for my iOS app. It needs to be a number/operator type keyboard, effectively a basic maths keyboard.
I have no idea how or where to start, I am relatively new to Swift/iOS development in general.
I have tried to use KeyboardKit, (See Github Page) but have had no luck; the documentation is extremely minimal, not anywhere close to enough for a beginner to take and use effectively.
I have tried many different GitHub repos, but none have fit what I need.
In a summary, my keyboard needs to be set as the default keyboard for the app, (but I'll ask this a a different question later); so it needs that option. It needs to be fully (easily =) customizable. And it needs to act just like the default Apple keyboard.
I am looking for something relevant, and extensible
Cheers!
I have solved my problem. I discovered this site
on StackOverflow that completely solved my issues. I was able to add customized keys to the keyboard, which was my primary issue.
I created a class called DigitButton.swift.
import UIKit
class DigitButton: UIButton {
var digit: Int = 0
}
class NumericKeyboard: UIView {
weak var target: (UIKeyInput & UITextInput)?
var useDecimalSeparator: Bool
lazy var parenthesis1: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = "("
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapParenthesis1(_:)), for: .touchUpInside)
return button
}()
lazy var squareroot: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = "√"
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapSquareRoot(_:)), for: .touchUpInside)
return button
}()
lazy var parenthesis2: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = ")"
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapParenthesis2), for: .touchUpInside)
return button
}()
lazy var exponentButton: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = "^0"
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapExponentButton(_:)), for: .touchUpInside)
return button
}()
lazy var exponentButton2: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = "^2"
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapExponentButton2(_:)), for: .touchUpInside)
return button
}()
lazy var exponentButton3: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = "^3"
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapExponentButton3(_:)), for: .touchUpInside)
return button
}()
lazy var exponentButton4: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = "^"
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapExponentButton4(_:)), for: .touchUpInside)
return button
}()
lazy var addButton: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = "+"
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapAddButton(_:)), for: .touchUpInside)
return button
}()
lazy var subtractButton: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = "-"
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapSubtractButton(_:)), for: .touchUpInside)
return button
}()
lazy var divideButton: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = "/"
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapDivideButton(_:)), for: .touchUpInside)
return button
}()
lazy var multiplyButton: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = "*"
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapMultiplyButton(_:)), for: .touchUpInside)
return button
}()
lazy var numericButtons: [DigitButton] = (0...9).map {
let button = DigitButton(type: .system)
button.digit = $0
button.setTitle("\($0)", for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .title1)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.sizeToFit()
button.titleLabel?.numberOfLines = 1
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.titleLabel?.lineBreakMode = .byTruncatingTail
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.addTarget(self, action: #selector(didTapDigitButton(_:)), for: .touchUpInside)
button.inputView.self?.sizeToFit()
return button
}
var deleteButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("⌫", for: .normal)
button.titleLabel?.font = .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 & UITextInput, useDecimalSeparator: Bool = false) {
self.target = target
self.useDecimalSeparator = useDecimalSeparator
super.init(frame: .zero)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Actions
extension NumericKeyboard {
#objc func didTapSquareRoot(_ sender: DigitButton) {
insertText("√")
}
#objc func didTapParenthesis1(_ sender: DigitButton) {
insertText("(")
}
#objc func didTapParenthesis2(_ sender: DigitButton) {
insertText(")")
}
#objc func didTapDigitButton(_ sender: DigitButton) {
insertText("\(sender.digit)")
}
#objc func didTapDecimalButton(_ sender: DigitButton) {
insertText(Locale.current.decimalSeparator ?? ".")
}
#objc func didTapExponentButton(_ sender: DigitButton){
insertText("^0")
}
#objc func didTapExponentButton2(_ sender: DigitButton){
insertText("^2")
}
#objc func didTapExponentButton3(_ sender: DigitButton){
insertText("^3")
}
#objc func didTapExponentButton4(_ sender: DigitButton){
insertText("^")
}
#objc func didTapAddButton(_ sender: DigitButton){
insertText("+")
}
#objc func didTapSubtractButton(_ sender: DigitButton){
insertText("-")
}
#objc func didTapDivideButton(_ sender: DigitButton){
insertText("/")
}
#objc func didTapMultiplyButton(_ sender: DigitButton){
insertText("*")
}
#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)
subStackView.addArrangedSubview(numericButtons[0])
subStackView.addArrangedSubview(parenthesis1)
subStackView.addArrangedSubview(parenthesis2)
subStackView.addArrangedSubview(squareroot)
subStackView.addArrangedSubview(addButton)
subStackView.addArrangedSubview(subtractButton)
subStackView.addArrangedSubview(multiplyButton)
subStackView.addArrangedSubview(divideButton)
subStackView.addArrangedSubview(exponentButton)
subStackView.addArrangedSubview(exponentButton2)
subStackView.addArrangedSubview(exponentButton4)
subStackView.addArrangedSubview(deleteButton)
}
func createStackView(axis: NSLayoutConstraint.Axis) -> UIStackView {
let stackView = UIStackView()
stackView.axis = axis
stackView.alignment = .fill
stackView.distribution = .fillProportionally
return stackView
}
func insertText(_ string: String) {
guard let range = target?.selectedRange else { return }
if let textField = target as? UITextField, textField.delegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) == false {
return
}
if let textView = target as? UITextView, textView.delegate?.textView?(textView, shouldChangeTextIn: range, replacementText: string) == false {
return
}
target?.insertText(string)
}
}
// MARK: - UITextInput extension
extension UITextInput {
var selectedRange: NSRange? {
guard let textRange = selectedTextRange else { return nil }
let location = offset(from: beginningOfDocument, to: textRange.start)
let length = offset(from: textRange.start, to: textRange.end)
return NSRange(location: location, length: length)
}
}
Then I set my InputView input method with textField.inputView = NumericKeyboard(target: textField)
This has worked perfectly.
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
}()
I want to include a voting function similar to the reddit app. I think a UIStackView would be perfect for this. Now I'm struggling to make the label between the vote-up and vote-down button display it's text.
I've tried to change the contentCompression to .fittingSizeLevel and to .defaultHigh but this seems to change nothing. On the image you can see, there would be plenty of space to fit the whole text, but it doesn't. What aspect am I missing?
class VotingStackView: UIStackView {
let arrowUpButton: UIButton = {
let button = UIButton()
button.backgroundColor = .clear
button.tintColor = GSSettings.UI.Colors.tintColor
button.imageView?.contentMode = .scaleAspectFit
button.contentEdgeInsets = UIEdgeInsets(top: 16, left: 0, bottom: 16, right: 0)
button.clipsToBounds = true
return button
}()
let arrowDownButton: UIButton = {
let button = UIButton()
button.backgroundColor = .clear
button.tintColor = GSSettings.UI.Colors.tintColor
button.imageView?.contentMode = .scaleAspectFit
button.contentEdgeInsets = UIEdgeInsets(top: 16, left: 0, bottom: 16, right: 0)
button.clipsToBounds = true
return button
}()
let percentageLabel: UILabel = {
let label = UILabel()
label.text = "100%"
label.textColor = GSSettings.UI.Colors.regularTextColor
label.font = GSSettings.UI.Fonts.helveticaLight?.withSize(20)
label.clipsToBounds = false
label.setContentCompressionResistancePriority(UILayoutPriority.fittingSizeLevel, for: .horizontal)
return label
}()
var views: [UIView] = [UIView]()
//MARK: - Init & View Loading
override init(frame: CGRect) {
super.init(frame: frame)
views = [arrowUpButton, percentageLabel ,arrowDownButton]
setupStackView()
setupImages()
setupSubviews()
}
//MARK: - Setup
func setupStackView() {
self.axis = .horizontal
self.spacing = 0
self.alignment = .center
self.distribution = .fillEqually
}
func setupImages() {
let upImage = UIImage(named: "arrow_up")
let downImage = UIImage(named: "arrow_down")
arrowUpButton.setImage(upImage, for: .normal)
arrowDownButton.setImage(downImage, for: .normal)
}
func setupSubviews() {
for view in views {
addArrangedSubview(view)
}
layoutIfNeeded()
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The votingStackView is part of another StackView:
class BottomStackView: UIStackView {
let votingStackView: VotingStackView = {
let stackview = VotingStackView()
return stackview
}()
let addFriendButton: UIButton = {
let button = UIButton()
button.backgroundColor = .clear
button.tintColor = GSSettings.UI.Colors.tintColor
button.setImage(UIImage(named: "plus")?.withRenderingMode(.alwaysTemplate), for: .normal)
return button
}()
let redView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
var views: [UIView] = [UIView]()
//MARK: - Init & View Loading
override init(frame: CGRect) {
super.init(frame: frame)
views = [votingStackView, addFriendButton, redView]
setupStackView()
setupSubviews()
}
//MARK: - Setup
func setupStackView() {
self.axis = .horizontal
self.spacing = 0
self.alignment = .leading
self.distribution = .fillEqually
}
func setupSubviews() {
for view in views {
addArrangedSubview(view)
}
layoutIfNeeded()
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Change
label.setContentCompressionResistancePriority(UILayoutPriority.fittingSizeLevel, for: .horizontal)
to
label.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: .horizontal)
From the docs it seems that fittingSizeLevel does not take into account parent constraints, so using defaultHigh seems like the safer option.