I want to programmatically set multiple alignment combinations for my button title, like so:
The easiest way I found to do this was to add two UILabels as subviews of my custom UIButton and set autolayout constraints accordingly.
However, I can't figure out how to make my labels behave the same way as a button title would (namely having its alpha altered when a button tap occurs).
I have tried setting their alpha property in a target-action method for .touchUpInside but since they're not attached to the button state, they won't change their alpha back to normal when the user ends tapping the button.
class Button: UIView {
var leftButton = UIButton(type: .roundedRect)
var rightButton = UIButton(type: .roundedRect)
init() {
super.init(frame: .zero)
createButton()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
createButton()
}
func createButton() {
self.backgroundColor = UIColor.cyan
self.layer.borderWidth = 0.5
self.layer.borderColor = UIColor.blue.cgColor
self.addSubview(leftButton)
self.addSubview(rightButton)
leftButton.setTitle("left", for: .normal)
leftButton.setTitleColor(UIColor.black, for: .normal)
leftButton.layer.borderColor = UIColor.yellow.cgColor
leftButton.layer.borderWidth = 0.5
leftButton.translatesAutoresizingMaskIntoConstraints = false
leftButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
leftButton.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
leftButton.trailingAnchor.constraint(lessThanOrEqualTo: rightButton.leadingAnchor).isActive = true
leftButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
rightButton.setTitle("right", for: .normal)
rightButton.layer.borderColor = UIColor.green.cgColor
rightButton.layer.borderWidth = 0.5
rightButton.setTitleColor(UIColor.black, for: .normal)
rightButton.translatesAutoresizingMaskIntoConstraints = false
rightButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
rightButton.leadingAnchor.constraint(greaterThanOrEqualTo: leftButton.trailingAnchor).isActive = true
rightButton.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
rightButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
leftButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
rightButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
func buttonTapped() {
print("button tapped")
}
there is a better approach.
Take two buttons and keep them in a stackView and do necessary settings.
But make sure that two buttons will point to same action.So that it will seem to user that it's actually one button.
here is the output.
I just made this in storyboard. As i am using stackView so programmatically creating and layouting is super easy. I think you can do it. :)
To stop flashing:
Make each button type custom.
How to make only one button action for both button?
See this popular stack answer.
Here is the result.
What you need is to differentiate between tap and hold and release of the button states. Luckily there are 2 built-in action methods that we can make use of for this.
Touch Down Action: This will be called as soon as you tap on the button. So you can change the alpha of your labels here.
Touch Up Inside: This action method is called when you have released the button. So you can reset your alpha here.
#IBAction func touchupInsideAction(_ sender: Any) {
firstLabel.alpha = 1.0
secondLabel.alpha = 1.0
}
#IBAction func touchDownAction(_ sender: Any) {
firstLabel.alpha = 0.5
secondLabel.alpha = 0.5
}
I was able to put the two labels and get them highlighted when clicked by creating a UIButton other than .custom (for this example I used .roundedRect), setting a dummy title, adding subviews to the button title itself and setting their constraints as I wanted to. It works as expected.
Code:
class Button: UIView {
let button: UIButton
let leftLabel = UILabel()
let rightLabel = UILabel()
init() {
button = UIButton(type: .roundedRect)
super.init(frame: .zero)
createButton()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createButton() {
self.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
button.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
button.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
button.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
button.backgroundColor = UIColor.cyan
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.blue.cgColor
button.setTitle(" ", for: .normal)
if let title = button.titleLabel {
title.addSubview(rightLabel)
rightLabel.text = "right"
rightLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.topAnchor.constraint(equalTo: title.topAnchor).isActive = true
rightLabel.leadingAnchor.constraint(greaterThanOrEqualTo: title.trailingAnchor).isActive = true
rightLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
rightLabel.bottomAnchor.constraint(equalTo: title.bottomAnchor).isActive = true
title.addSubview(leftLabel)
leftLabel.text = "left"
leftLabel.translatesAutoresizingMaskIntoConstraints = false
leftLabel.topAnchor.constraint(equalTo: title.topAnchor).isActive = true
leftLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
leftLabel.trailingAnchor.constraint(lessThanOrEqualTo: title.leadingAnchor).isActive = true
leftLabel.bottomAnchor.constraint(equalTo: title.bottomAnchor).isActive = true
}
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
func buttonTapped() {
print("button tapped")
}
Related
I need to add a UIButton as a subview on a UIView but it actually doesn't appear at runtime.
This is my code:
let moreButton : UIButton = {
let button = UIButton()
button.setImage(#imageLiteral(resourceName: "more"), for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(moreButton)
moreButton.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
moreButton.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
moreButton.heightAnchor.constraint(equalToConstant: 30).isActive = true
moreButton.widthAnchor.constraint(equalToConstant: 20).isActive = true
}
The button isn't added eventually to the view. I'm sure this is an easy fix but I can't wrap my head around it.
First of all, make sure you the viewController is showing anything since you're doing it without storyboards, check out this simple tutorial:
https://medium.com/better-programming/creating-a-project-without-storyboard-in-2020-and-without-swifui-82080eb6d13b
If the problem is with that UIButton, try to set up the subviews in viewDidLoad:
final class ViewController: UIViewController {
let cardView = CardView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(cardView)
/// Constraints
let margins = view.layoutMarginsGuide
cardView.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
cardView.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
cardView.topAnchor.constraint(equalTo: margins.topAnchor).isActive = true
cardView.heightAnchor.constraint(equalToConstant: 30).isActive = true
cardView.widthAnchor.constraint(equalToConstant: 20).isActive = true
}
}
final class CardView: UIView {
let moreButton : UIButton = {
let button = UIButton()
button.setTitle("Button title", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
override init(frame: CGRect) {
super.init(frame:frame)
self.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(moreButton)
/// Constraints
let margins = self.layoutMarginsGuide
moreButton.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
moreButton.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
moreButton.topAnchor.constraint(equalTo: margins.topAnchor).isActive = true
moreButton.heightAnchor.constraint(equalToConstant: 30).isActive = true
moreButton.widthAnchor.constraint(equalToConstant: 20).isActive = true
}
}
I've set up margins for the constraints and also added a leadingAnchor constraint, that might have been the issue as well.
I want to have a UIButton in a UIViewController tappable while the user is editing text in a UISearchBar that is part of the UINavigationBar.
The UIButton is responding to touches (it shows the highlighted state), but it is not calling its action. Same problem for other UIControl classes like UISwitch.
To make it more intriguing: when placing the UISearchBar in the UIViewController's UIView, there's no problem.
What should I do in order to trigger the button action while the UISearchBar is active?
Example VC:
import UIKit
class ViewController: UIViewController {
private let button: UIButton = {
let button = UIButton(type: UIButtonType.system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Test", for: .normal)
button.setTitleColor(.green, for: .normal)
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
return button
}()
private let textField: UITextField = {
let textField = UITextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.placeholder = "place"
textField.borderStyle = .bezel
textField.addTarget(self, action: #selector(textFieldTriggered), for: .allEditingEvents)
return textField
}()
private let uiSwitch: UISwitch = {
let uiSwitch = UISwitch()
uiSwitch.translatesAutoresizingMaskIntoConstraints = false
uiSwitch.addTarget(self, action: #selector(switchToggled), for: UIControlEvents.valueChanged)
return uiSwitch
}()
private let formSearchBar: UISearchBar = {
let searchBar = UISearchBar()
searchBar.translatesAutoresizingMaskIntoConstraints = false
return searchBar
}()
private let navigationSearchBar: UISearchBar = {
return UISearchBar()
}()
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(button)
view.addSubview(textField)
view.addSubview(formSearchBar)
view.addSubview(uiSwitch)
NSLayoutConstraint.activate([
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: -16),
view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: button.topAnchor, constant: -16),
button.bottomAnchor.constraint(equalTo: textField.topAnchor, constant: -16),
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: textField.leadingAnchor, constant: -16),
textField.widthAnchor.constraint(equalToConstant: 100),
textField.bottomAnchor.constraint(equalTo: formSearchBar.topAnchor, constant: -16),
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: formSearchBar.leadingAnchor, constant: -16),
formSearchBar.widthAnchor.constraint(equalToConstant: 100),
formSearchBar.bottomAnchor.constraint(equalTo: uiSwitch.topAnchor, constant: -16),
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: uiSwitch.leadingAnchor, constant: -16),
])
navigationItem.titleView = navigationSearchBar
_ = navigationSearchBar.becomeFirstResponder()
}
#objc
private func buttonTapped() {
let color = button.titleColor(for: .normal)
button.setTitleColor(color == .green ? .red : .green, for: .normal)
}
#objc
private func textFieldTriggered() {
textField.placeholder = textField.placeholder == "place" ? "holder" : "place"
}
#objc
private func switchToggled() {
UIView.animate(withDuration: 0.3) {
self.uiSwitch.isOn = !self.uiSwitch.isOn
}
}
}
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
}
}
I have a rounded likeButton that is behind a rounded moreButton. The likeButton is pinned to the centerX and centerY of the moreButton. When I press the moreButton I want to animate the likeButton 200 points above the moreButton.
I use a NSLayoutConstraint to keep track of the likeButton's centerY and to make changes to it. When I call UIView.animate and inside it's closure I call self.view.layoutIfNeeded() the view isn't updating.
#objc func moreButtonTapped(){
likeButtonCenterY?.isActive = false
likeButton.centerYAnchor.constraint(equalTo: self.moreButton.centerYAnchor, constant: -200)
likeButtonCenterY?.isActive = true
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
Where am I going wrong?
let moreButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(named: "more"), for: .normal)
button.tintColor = .red
button.clipsToBounds = true
button.addTarget(self, action: #selector(moreButtonTapped), for: .touchUpInside)
return button
}()
let likeButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(named: "like"), for: .normal)
button.tintColor = .blue
button.clipsToBounds = true
return button
}()
var likeButtonCenterY: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setAnchors()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
moreButton.layer.cornerRadius = moreButton.frame.size.width / 2
likeButton.layer.cornerRadius = likeButton.frame.size.width / 2
}
#objc func moreButtonTapped(){
likeButtonCenterY?.isActive = false
likeButton.centerYAnchor.constraint(equalTo: self.moreButton.centerYAnchor, constant: -200)
likeButtonCenterY?.isActive = true
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
func setAnchors(){
view.addSubview(likeButton)
view.addSubview(moreButton)
moreButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
moreButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -200).isActive = true
moreButton.widthAnchor.constraint(equalToConstant: 100).isActive = true
moreButton.heightAnchor.constraint(equalToConstant: 100).isActive = true
likeButtonCenterY = likeButton.centerYAnchor.constraint(equalTo: moreButton.centerYAnchor)
likeButtonCenterY?.isActive = true
likeButton.centerXAnchor.constraint(equalTo: moreButton.centerXAnchor).isActive = true
likeButton.widthAnchor.constraint(equalToConstant: 100).isActive = true
likeButton.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
Try this
#objc func moreButtonTapped(){
likeButtonCenterY?.isActive = false
likeButtonCenterY = likeButton.centerYAnchor.constraint(equalTo: self.moreButton.centerYAnchor, constant: -200)
likeButtonCenterY?.isActive = true
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
You have forgotten to write likeButtonCenterY =
If you are activating/deactivating multiple constraints at the same time (like a batch), you might need to override the updateConstraints() method. More information is here: https://developer.apple.com/documentation/uikit/uiview/1622512-updateconstraints
Basically your code in the moreButtonTapped() will go in the overridden implementation of the updateConstraints() method. Don't forget to call super() in the last line of the method.
Then your animation code should look like look like:
UIView.animate(withDuration: 0.3) {
self.view.setNeedsUpdateConstraints()
self.view.layoutIfNeeded()
})
I have a custom view subclass that I will provide all the code of for clarity. I have highlighted the relevant parts below.
Note: I know how to animate views using AutoLayout. The problem is not writing the animation code. The problem is that it updates the view but doesn't actually animate anything. It just jumps to the new size.
class ExpandingButtonView: UIView {
let titleLabel: UILabel = {
let l = UILabel()
l.translatesAutoresizingMaskIntoConstraints = false
l.textColor = .white
l.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .vertical)
l.setContentHuggingPriority(UILayoutPriorityRequired, for: .vertical)
return l
}()
let buttonStack: UIStackView = {
let s = UIStackView()
s.translatesAutoresizingMaskIntoConstraints = false
s.axis = .vertical
s.spacing = 8
s.isLayoutMarginsRelativeArrangement = true
s.layoutMargins = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
return s
}()
var collapsed: Bool = true {
didSet {
animatedCollapsedState()
}
}
lazy var collapsedConstraint: NSLayoutConstraint = {
return self.bottomAnchor.constraint(equalTo: self.titleLabel.bottomAnchor, constant: 10)
}()
lazy var expandedConstraint: NSLayoutConstraint = {
return self.bottomAnchor.constraint(equalTo: self.buttonStack.bottomAnchor)
}()
init(title: String, color: UIColor, buttonTitles: [String]) {
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
layer.cornerRadius = 8
clipsToBounds = true
backgroundColor = color
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(toggleCollapsed))
tapGestureRecognizer.numberOfTapsRequired = 1
tapGestureRecognizer.numberOfTouchesRequired = 1
addGestureRecognizer(tapGestureRecognizer)
titleLabel.text = title
addSubview(titleLabel)
addSubview(buttonStack)
buttonTitles.forEach {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = UIColor(white: 1.0, alpha: 0.5)
button.setTitle($0, for: .normal)
button.tintColor = .white
button.layer.cornerRadius = 4
button.clipsToBounds = true
button.titleLabel?.font = .boldSystemFont(ofSize: 17)
button.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .vertical)
buttonStack.addArrangedSubview(button)
}
NSLayoutConstraint.activate([
collapsedConstraint,
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 10),
titleLabel.bottomAnchor.constraint(equalTo: buttonStack.topAnchor),
buttonStack.leadingAnchor.constraint(equalTo: leadingAnchor),
buttonStack.trailingAnchor.constraint(equalTo: trailingAnchor),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func toggleCollapsed() {
collapsed = !collapsed
}
func animatedCollapsedState() {
UIView.animate(withDuration: 1) {
self.collapsedConstraint.isActive = self.collapsed
self.expandedConstraint.isActive = !self.collapsed
self.layoutIfNeeded()
}
}
}
It has two states...
Collapsed...
Expanded...
When you tap it the tapGestureRecognizer toggles the collapsed value which triggers the didSet which then animates the UI.
The animating function is...
func animatedCollapsedState() {
UIView.animate(withDuration: 1) {
self.collapsedConstraint.isActive = self.collapsed
self.expandedConstraint.isActive = !self.collapsed
self.layoutIfNeeded()
}
}
However... it is not animating. It just jumps to the new size without actually animating.
I have removed another part of the view for the question. There is also a background image view that fades in/out during the UI change. That DOES animate. So I'm not quite sure what's going on here?
I have also tried moving the constraint updates out of the animation block and also tried running layoutIfNeeded() before updating them.
In all cases it does the same thing jumping to the new size.
You have to call layoutIfNeeded() on the view's superview.
func animatedCollapsedState() {
self.collapsedConstraint.isActive = self.collapsed
self.expandedConstraint.isActive = !self.collapsed
UIView.animate(withDuration: 1) {
self.superview?.layoutIfNeeded()
}
}