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.
Related
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:
recently, I decided to quit using storyboards on my IOS app. So, am presently learning everything now with code. I was trying to place a label below navigationBar view but I got an error and I don't know how to debug it. Please view my code and share your thoughts.
class SellBaseViewController: UIViewController {
lazy var container: UIStackView = {
let stackView = UIStackView(frame: .zero)
stackView.alignment = .fill
stackView.axis = .vertical
stackView.spacing = 2
stackView.willSetConstraints()
return stackView
}()
lazy var navHeader: UIView! = {
return self.navBar()
}()
lazy var firstLabel: UILabel! = {
return self.labelOne()
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if !Authentication.shared.isAuthenticated {
showLogin()
} else {
self.setupInterface()
}
}
private func setupInterface() {
self.navigationController?.navigationBar.isHidden = true
self.embedInScrollView(content: self.container)
navHeader.willSetConstraints()
firstLabel.willSetConstraints()
self.container.addArrangedSubviews([self.navHeader!, self.firstLabel!])
DispatchQueue.main.async {
NSLayoutConstraint.activate([
self.navHeader.heightAnchor.constraint(equalToConstant: 44),
self.navHeader.widthAnchor.constraint(equalTo: self.view.widthAnchor),
self.navHeader.topAnchor.constraint(equalTo: self.container.topAnchor),
])
}
}
// MARK: NAVBAR
func navBar() -> UIView {
let navBar = UIView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: self.view.frame.width, height: 44)))
navBar.backgroundColor = UIColor.constants.darkBlue
let backIcon = UIImage(named: "ic_back")?.withRenderingMode(.alwaysTemplate)
let returnButton = UIButton(type: .custom)
returnButton.imageView?.tintColor = UIColor.white
returnButton.setImage(backIcon, for: .normal)
returnButton.image(for: .normal)
returnButton.titleLabel?.font = UIFont(name: "Hind", size: 18)
returnButton.setTitle("Sell", for: .normal)
returnButton.setTitleColor(UIColor.white, for: .normal)
returnButton.addTarget(self, action: #selector(self._return), for: .touchUpInside)
returnButton.willSetConstraints()
navBar.addSubviews([returnButton])
NSLayoutConstraint.activate([
returnButton.centerYAnchor.constraint(equalTo: navBar.centerYAnchor),
returnButton.leadingAnchor.constraint(equalTo: navBar.leadingAnchor, constant: 11),
returnButton.heightAnchor.constraint(equalToConstant: 24),
returnButton.widthAnchor.constraint(equalToConstant: 71),
])
return navBar
}
func labelOne() -> UILabel{
let label = UILabel()
label.textAlignment = .center
label.textColor = .white
label.font = UIFont(name: "Avenir-Light", size: 15.0)
label.text = "This is a Label"
self.view.addSubview(label)
return labelOne()
}
#objc func _return() {
self.backHome()
}
}
The navBar showed well, but when I added the label, the app kept crashing with this error. I don't know how to find out exactly what the error is:
Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee6703fe8)
func labelOne() -> UILabel{
let label = UILabel()
label.textAlignment = .center
label.textColor = .white
label.font = UIFont(name: "Avenir-Light", size: 15.0)
label.text = "This is a Label"
self.view.addSubview(label)
return labelOne()
}
You did:
return labelOne()
You should do:
return label
I've got two text views in each of my collection view cells, I've made a custom class for the cells. here is the code:
class CustomWriterPageCell: UICollectionViewCell {
fileprivate let textViewOne: UITextView = {
let tv = UITextView()
tv.backgroundColor = .cyan
tv.text = "Chapter Title"
tv.font = UIFont(name: "Avenir-Roman", size: 27)
tv.textColor = .gray
return tv
}()
fileprivate let textViewTwo: UITextView = {
let tv = UITextView()
tv.textColor = .gray
tv.text = "Start your story..."
tv.textContainerInset = UIEdgeInsets(top: 20, left: 15, bottom: 20, right: 15)
tv.font = UIFont(name: "Avenir-Book", size: 23)
tv.backgroundColor = .black
return tv
}()
}
I would like to add placeholders to both these text views but the problem is that since they are in a custom class there is no way, that I know of, to find out which text view is being edited so I can't add the respective placeholders back when textViewDidEndEditing takes place, is there a way to find out which text view is being edited? Is there a way to access the text views from the main class?
In your CustomWriterPageCell class:
fileprivate let textViewOne: UITextView = {
let tv = UITextView()
tv.backgroundColor = .cyan
tv.text = "Chapter Title"
tv.font = UIFont(name: "Avenir-Roman", size: 27)
tv.textColor = .gray
return tv
}()
fileprivate let textViewTwo: UITextView = {
let tv = UITextView()
tv.textColor = .gray
tv.text = "Start your story..."
tv.textContainerInset = UIEdgeInsets(top: 20, left: 15, bottom: 20, right: 15)
tv.font = UIFont(name: "Avenir-Book", size: 23)
tv.backgroundColor = .black
return tv
}()
override init(frame: CGRect) {
super.init(frame: frame)
textViewOne.delegate = self
textViewTwo.delegate = self
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then:
extension CustomWriterPageCell : UITextViewDelegate {
func textViewDidEndEditing(_ textView: UITextView) {
if textView == textViewOne {
textView.text = "..."
}
else if textView == textViewTwo {
textView.text = "..."
}
}
}
This way, your View Controller still does not need to know about your text views.
You can always access the actual textView and know which one is being edited in delegate methods, just like this:
class ViewController: UIViewController {
var textViewA: UITextView = .init()
var textViewB: UITextView = .init()
override func viewDidLoad() {
super.viewDidLoad()
textViewA.delegate = self
textViewB.delegate = self
}
}
extension ViewController: UITextViewDelegate {
func textViewDidEndEditing(_ textView: UITextView) {
switch textView {
case textViewA:
textView.text = "Placeholder A"
case textViewB:
textView.text = "Placeholder B"
default:
break
}
}
}
Also, because of the fact that UITextView doesn't have any built-in placeholder mechanism - you could use this nice pod: https://github.com/devxoul/UITextView-Placeholder
If you use this pod and don't want to make textViews public/internal - you can create computedProperties for it:
var placeholder: String {
get {
return textView.placeholder
} set {
textView.placeholder = newValue
}
}
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.
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
}()