UIGestureRecognizer not detected on subview - ios

I made a UIView subclass and added a circle view on top right corner of the view. Then I added UIPanGestureRecognizer to the circle view.
The problem is that gesture is only recognized on left bottom part of circle where the circle is located over the super view.
How can I make entire circle to property detected gesture?
Following is entire class code of UIView subclass I made.
import UIKit
class ResizableImageView: UIView {
private let circleWidth: CGFloat = 40
var themeColor: UIColor = UIColor.magentaColor()
lazy var cornerCircle: UIView = {
let v = UIView()
v.layer.cornerRadius = self.circleWidth / 2
v.layer.borderWidth = 1
v.layer.borderColor = self.themeColor.CGColor
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(buttonTouchMoved) )
v.addGestureRecognizer(panGesture)
return v
}()
// Init
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
configureSelf()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupSubviews() {
// Add cornerButton to self and set auto layout
addSubview( cornerCircle )
addConstraintsWithFormat("H:[v0(\(circleWidth))]", views: cornerCircle) // Extension code for setting auto layout
addConstraintsWithFormat("V:[v0(\(circleWidth))]", views: cornerCircle)
addConstraint(NSLayoutConstraint(item: cornerCircle, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0))
addConstraint(NSLayoutConstraint(item: cornerCircle, attribute: .CenterY, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0))
}
func configureSelf() {
// Set border
layer.borderWidth = 1
layer.borderColor = themeColor.CGColor
}
// Gesture Event
func buttonTouchMoved(gestureRecognizer: UIPanGestureRecognizer) {
let point = gestureRecognizer.locationInView(self)
print(point)
}
}
ViewController
import UIKit
class ImageViewCheckController: UIViewController {
let imageView: ResizableImageView = {
let iv = ResizableImageView()
return iv
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "ImageViewCheck"
view.backgroundColor = UIColor.whiteColor()
setupSubviews()
}
func setupSubviews() {
view.addSubview( imageView )
view.addConstraintsWithFormat("V:|-100-[v0(200)]", views: imageView)
view.addConstraintsWithFormat("H:|-50-[v0(100)]", views: imageView)
}
}

Normally, there is no touch on a subview outside the bounds of its superview.
To change this, you will have to override hitTest(point:withEvent:) on the superview to alter the way hit-testing works:
override func hitTest(point: CGPoint, withEvent e: UIEvent?) -> UIView? {
if let result = super.hitTest(point, withEvent:e) {
return result
}
for sub in self.subviews.reverse() {
let pt = self.convertPoint(point, toView:sub)
if let result = sub.hitTest(pt, withEvent:e) {
return result
}
}
return nil
}

Related

Self sizing cell with multiple Stack Views

I've looked all over the forum and attempted all the solutions and thus far nothing has worked. I noticed my UIImageView was overlaying multiple cells, meaning the celll did not automatically adjust its height. Here is the constraint i found in the console it complained about.
"<NSLayoutConstraint:0x600001970f50 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x7f86a4813dd0.height == 44 (active)>"
In my tableViewController I have the follow
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 300
Here is my entire cell that should self size.
import UIKit
class UserConnectionCell: UITableViewCell {
fileprivate let leftImageView: UIImageView = {
let uiImageView = UIImageView()
uiImageView.translatesAutoresizingMaskIntoConstraints = false
return uiImageView
}()
fileprivate let leftLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
fileprivate let middleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.font = UIFont(name: "Ariel", size: 10)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
fileprivate let rightImageView: UIImageView = {
let uiImageView = UIImageView()
uiImageView.translatesAutoresizingMaskIntoConstraints = false
return uiImageView
}()
fileprivate let rightLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
fileprivate let stackViewLeft: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
fileprivate let stackViewRight: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
fileprivate let stackViewMain: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.spacing = 0
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
//
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier:reuseIdentifier)
stackViewLeft.addArrangedSubview(leftImageView)
stackViewLeft.addArrangedSubview(leftLabel)
stackViewRight.addArrangedSubview(rightImageView)
stackViewRight.addArrangedSubview(rightLabel)
stackViewMain.addArrangedSubview(stackViewLeft)
stackViewMain.addArrangedSubview(middleLabel)
stackViewMain.addArrangedSubview(stackViewRight)
contentView.addSubview(stackViewMain)
}
// called when trying to layout subviews.
override func layoutSubviews() {
super.layoutSubviews()
stackViewLeft.addConstraint(NSLayoutConstraint(item: leftImageView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 100))
stackViewLeft.addConstraint(NSLayoutConstraint(item: leftImageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 100))
stackViewRight.addConstraint(NSLayoutConstraint(item: rightImageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 100))
stackViewRight.addConstraint(NSLayoutConstraint(item: rightImageView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 100))
NSLayoutConstraint.activate(
[stackViewMain.topAnchor.constraint(equalTo: contentView.topAnchor,constant: 0),
stackViewMain.leadingAnchor.constraint(equalTo: contentView.leadingAnchor,constant: 0),
stackViewMain.trailingAnchor.constraint(equalTo: contentView.trailingAnchor,constant: 0),
stackViewMain.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var viewModel : UserConnectionViewModel? {
didSet {
// move this to the view model
if let profileUrl = viewModel?.leftImageUrl {
leftImageView.loadImageFromURL(url: profileUrl)
} else {
leftImageView.image = UIImage(named: "defaultprofile")
}
if let profileUrl = viewModel?.rightImageUrl {
rightImageView.loadImageFromURL(url: profileUrl)
} else {
rightImageView.image = UIImage(named: "defaultprofile")
}
leftLabel.text = viewModel?.leftLabel
middleLabel.text = viewModel?.middleLabel
rightLabel.text = viewModel?.rightlabel
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.contentView.autoresizingMask = .flexibleHeight
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Any ideas for why the cell is not self sizing?
First, a cell's contentView is a "special" view with properties integral to the table view's operation.
So, do not do this:
self.contentView.autoresizingMask = .flexibleHeight
Second, layoutSubviews() can be (and usually is) called multiple times during the lifecycle of a cell / view. Your constraint setup should be done in init:
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier:reuseIdentifier)
stackViewLeft.addArrangedSubview(leftImageView)
stackViewLeft.addArrangedSubview(leftLabel)
stackViewRight.addArrangedSubview(rightImageView)
stackViewRight.addArrangedSubview(rightLabel)
stackViewMain.addArrangedSubview(stackViewLeft)
stackViewMain.addArrangedSubview(middleLabel)
stackViewMain.addArrangedSubview(stackViewRight)
contentView.addSubview(stackViewMain)
NSLayoutConstraint.activate([
// constrain main stack view to all 4 sides of contentView
stackViewMain.topAnchor.constraint(equalTo: contentView.topAnchor,constant: 0),
stackViewMain.leadingAnchor.constraint(equalTo: contentView.leadingAnchor,constant: 0),
stackViewMain.trailingAnchor.constraint(equalTo: contentView.trailingAnchor,constant: 0),
stackViewMain.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0),
// constrain left image view Width: 100-pts,
// Height equal to Width (1:1 ratio)
leftImageView.widthAnchor.constraint(equalToConstant: 100.0),
leftImageView.heightAnchor.constraint(equalTo: leftImageView.widthAnchor),
// constrain right image view Width: 100-pts,
// Height equal to Width (1:1 ratio)
rightImageView.widthAnchor.constraint(equalToConstant: 100.0),
rightImageView.heightAnchor.constraint(equalTo: rightImageView.widthAnchor),
])
}
So... replace your init with the above code and completely remove both your awakeFromNib() and layoutSubviews() funcs.
You should get this:

How to add "inner padding" into custom view so that its size will become smaller visually yet able retain its touchable area?

I have the a custom view with the following simple constraint
Height 88
Width 88
Center X
Center Y
I tend to make it as circle visually. Here's my code.
extension UIView {
func asCircle() {
self.layer.cornerRadius = self.frame.width / 2;
self.layer.masksToBounds = true
}
}
class ViewController: UIViewController {
#IBOutlet weak var circleView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
circleView.backgroundColor = .red
circleView.asCircle()
// 1. create a gesture recognizer (tap gesture)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
// 2. add the gesture recognizer to a view
circleView.addGestureRecognizer(tapGesture)
// Currently, the touchable area of circleView is 88 x 88
}
// 3. this method is called when a tap is recognized
#objc func handleTap(sender: UITapGestureRecognizer) {
print("tap")
}
}
What I would like to have is
The touchable area of the circle remains 88 x 88
The red circle however will look visually as 22 x 22
I add the following code, but it doesn't make any change
// We use 33, because 88-33-33 = 22
circleView.layoutMargins = UIEdgeInsets(top: 33, left: 33, bottom: 33, right: 33)
Does anyone know how to achieve so? Preferable without overriding draw function, or add an additional UIView as subview.
Add layer into your circleView and clear the circleView
example:
extension UIView {
func asCircle() {
self.layer.cornerRadius = self.frame.width / 2;
self.layer.masksToBounds = true
}
}
class ViewController: UIViewController {
var circleView: UIView = {
let circle = UIView()
circle.translatesAutoresizingMaskIntoConstraints = false
return circle
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.circleView)
circleView.backgroundColor = .clear
self.circleView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
self.circleView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
let width = NSLayoutConstraint(item: self.circleView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1.0, constant: 88)
let height = NSLayoutConstraint(item: self.circleView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1.0, constant: 88)
self.circleView.addConstraints([width, height])
self.circleView.layoutIfNeeded()
// 1. create a gesture recognizer (tap gesture)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
// 2. add the gesture recognizer to a view
circleView.addGestureRecognizer(tapGesture)
// Currently, the touchable area of circleView is 88 x 88
}
// 3. this method is called when a tap is recognized
#objc func handleTap(sender: UITapGestureRecognizer) {
print("tap")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let layer = CALayer()
layer.backgroundColor = UIColor.blue.cgColor
layer.frame = CGRect(x: 0, y: 0, width: 22, height: 22)
self.circleView.layer.insertSublayer(layer, at: 1)
layer.cornerRadius = layer.frame.height / 2
layer.position = CGPoint(x: self.circleView.frame.height / 2, y: self.circleView.frame.height / 2)
}
}

IBDesignable element misplaced in interface builder, needs to update frames manually

I'm working on a Radio Button View, and already uploaded a test project on github at https://github.com/paulicelli/RadioButton
This is basically a subclass of a UIStackView, with a IBInspectable number of buttons to create some radio buttons.
Working on the device, but I always see a misplacement of the buttons when I open Interface Builder, until I change the number of buttons or the spacing. You can see the problem in view1, then the correct view in view2 attached.
QUESTION:
How can I tell Interface builder to properly position the buttons as soon as I open it?
here is the code:
import UIKit
// ROUNDBUTTON
final class RoundButton: UIButton {
var borderWidth: CGFloat = 2.0
var primaryColor: UIColor = UIColor.blue
var primaryColorWithAlpha: UIColor {
return primaryColor.withAlphaComponent(0.25)
}
override public var buttonType: UIButtonType {
return .custom
}
override public func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = bounds.size.width * 0.5
self.clipsToBounds = true
setupColors()
}
func setupColors() {
switch self.state {
case UIControlState.normal:
super.backgroundColor = .white
self.setTitleColor(primaryColor, for: state)
self.layer.borderColor = primaryColor.cgColor
self.layer.borderWidth = borderWidth
default:
super.backgroundColor = primaryColorWithAlpha
self.setTitleColor(.white, for: state)
}
}
}
// END OF ROUNDBUTTON
// PROTOCOL
#objc protocol RadioButtonsViewDelegate {
#objc optional func didSelectButton(_ aButton: RoundButton?)
}
// END OF PROTOCOL
// RADIOBUTTONSVIEW
#IBDesignable
class RadioButtonsView: UIStackView {
var shouldLetDeselect = true
var buttons = [RoundButton]()
weak var delegate : RadioButtonsViewDelegate? = nil
private(set) weak var currentSelectedButton: RoundButton? = nil
#IBInspectable
var number: Int = 2 {
didSet {
number = max(number, 2)
setupButtons()
}
}
func setupButtons() {
if !buttons.isEmpty {
buttons.forEach { self.removeArrangedSubview($0) }
buttons.removeAll()
}
for i in 0..<self.number {
let button = RoundButton()
button.setTitle("\(i)", for: .normal)
button.addTarget(self, action: #selector(pressed(_:)),
for: UIControlEvents.touchUpInside)
buttons.append(button)
}
}
func pressed(_ sender: RoundButton) {
if (sender.isSelected) {
if shouldLetDeselect {
sender.isSelected = false
currentSelectedButton = nil
}
}else {
buttons.forEach { $0.isSelected = false }
sender.isSelected = true
currentSelectedButton = sender
}
delegate?.didSelectButton?(currentSelectedButton)
}
override init(frame: CGRect) {
super.init(frame: frame)
setupButtons()
}
required init(coder: NSCoder) {
super.init(coder: coder)
setupButtons()
}
override var intrinsicContentSize: CGSize {
return CGSize(width: self.bounds.width,
height: UIViewNoIntrinsicMetric)
}
override func layoutSubviews() {
self.translatesAutoresizingMaskIntoConstraints = false
buttons.forEach { self.addArrangedSubview($0) }
let constraints: [NSLayoutConstraint] = {
var constraints = [NSLayoutConstraint]()
constraints.append(NSLayoutConstraint(item: buttons[0],
attribute: NSLayoutAttribute.width,
relatedBy: NSLayoutRelation.equal,
toItem: self,
attribute: NSLayoutAttribute.height,
multiplier: 1,
constant: 0)
)
for i in 1..<buttons.count {
constraints.append(NSLayoutConstraint(item: buttons[i],
attribute: NSLayoutAttribute.width,
relatedBy: NSLayoutRelation.equal,
toItem: buttons[i - 1],
attribute: NSLayoutAttribute.width,
multiplier: 1,
constant: 0)
)
}
return constraints
}()
self.addConstraints(constraints)
}
}
// END OF RADIOBUTTONSVIEW
EDIT:
As requested by #dfd I also tried:
#IBInspectable
var number: Int {
get {
setupButtons()
return self.number
}
set {
self.number = max(newValue, 2)
setupButtons()
}
}
and I got 2 errors while loading Interface Builder:
error: IB Designables: Failed to render and update auto layout status for ViewController (BYZ-38-t0r): The agent crashed
error: IB Designables: Failed to update auto layout status: The agent crashed

How can I add same uiview multiples times at different positions to a UIViewController?

I am trying to achieve this in swift.
So far I created my own custom view which is a subclass of UIView class:
class MyConnections: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
let context = UIGraphicsGetCurrentContext()
CGContextSetLineWidth(context, 1)
CGContextSetStrokeColorWithColor(context, UIColor.blackColor().CGColor)
let circle = CGRectMake(5, 60, 80, 80)
CGContextAddEllipseInRect(context, circle)
CGContextStrokePath(context)
CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor)
CGContextFillEllipseInRect(context, circle)
}
}
This is my view controller where I add the above view as a subview:
let profile = MyConnections()
override func viewDidLoad() {
super.viewDidLoad()
profile.backgroundColor = UIColor.clearColor()
view.addSubview(profile)
self.profile.setTranslatesAutoresizingMaskIntoConstraints(false)
//constraints for the location button
let horizontalConstraint = NSLayoutConstraint(item: self.profile, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Leading, multiplier: 1.0, constant: 10)
let verticalConstraint = NSLayoutConstraint(item: self.profile
, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 20)
let widthConstraint = NSLayoutConstraint(item: self.profile, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 150)
let heightConstraint = NSLayoutConstraint(item: self.profile, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 150)
self.view.addConstraints([verticalConstraint, horizontalConstraint, widthConstraint, heightConstraint])
// Do any additional setup after loading the view.
}
All the code above gives me a circle on top. Now I want to repeat that same circle multiple times at different positions as seen in the image. I can create multiple instances of the uiview add them as subview but every time I will have to define new constraints for it which I don't want to do.
Can anyone please help me and give me an efficient answer?
You should know a UIView can have a single superview/parent. If you add it as a subview at a different position (using addSubview method) it will be removed from the first position and added as a subview to the new position.
In your case to add more subviews you have to create more UIView objects not use a single global UIView.
If the layout is repetitive a UITableView / UICollectionView is a better choice.
Your requirement and UI qualify for a UICollectionView I think you should use UICollectionView and can create a custom UICollectionViewCell with round image and and badge view as well, and they add dataSource and delegate methods. That will not only help in creating UI but it will make your app more performant by reusing cells
Here is a nice tutorial about UICollectionView
Here's a simplified view to create collection view programmatically:
make the collection view and layout programmatically just like any other view you would code and add it as subview like below:
lazy var myCollectionView : UICollectionView = {
let layout = YourFlowLayout()
layout.scrollDirection = self.direction;
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
let cv = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
cv.dataSource = self
cv.delegate = self
cv.isPagingEnabled = true
cv.backgroundColor = UIColor.clear
cv.showsHorizontalScrollIndicator = false
cv.showsVerticalScrollIndicator = false
cv.allowsMultipleSelection = false
return cv
}()
and your flow layout could be something like:
mport UIKit
class Yourflowlayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return super.layoutAttributesForElements(in: rect)?.map {
attrs in
let attrscp = attrs.copy() as! UICollectionViewLayoutAttributes
self.applyLayoutAttributes(attributes: attrscp)
return attrscp
}
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if let attrs = super.layoutAttributesForItem(at: indexPath as IndexPath) {
let attrscp = attrs.copy() as! UICollectionViewLayoutAttributes
self.applyLayoutAttributes(attributes: attrscp)
return attrscp
}
return nil
}
func applyLayoutAttributes(attributes : UICollectionViewLayoutAttributes) {
if attributes.representedElementKind != nil {
return
}
if let collectionView = self.collectionView {
let stride = (self.scrollDirection == .horizontal) ? collectionView.frame.size.width : collectionView.frame.size.height
let offset = CGFloat(attributes.indexPath.section) * stride
var xCellOffset : CGFloat = CGFloat(attributes.indexPath.item) * self.itemSize.width
var yCellOffset : CGFloat = CGFloat(attributes.indexPath.item) * self.itemSize.height
if(self.scrollDirection == .horizontal) {
xCellOffset += offset;
} else {
yCellOffset += offset
}
attributes.frame = CGRect(x: xCellOffset, y: yCellOffset, width: self.itemSize.width, height: self.itemSize.height)
}
}
}
You can add the collectionView in your other classes as a subview , make sure you have the
myCollectionView.translatesAutoresizingMaskIntoConstraints = false so that your constrains are applied and you actually see the collection view and of course add your constrains or give it a frame.
Hope that helps someone.

Multiline UIButton and autolayout

I have created a view controller that looks like this:
I want the two top buttons to always have 20 points between themselves and the left/right edges of the whole view. They should always have the same width too. I have created the constraints for all of this and it works exactly how I want it to. The problem is the vertical constraints. The buttons should always be 20 points beneath the top edge. They should have the same height. However, autolayout doesn't respect that the left label needs two lines to fit all its text, so the result looks like this:
I want it to look like in the first picture. I can't add constant height constraints to the buttons because when the app runs on iPad, only one line is needed and it would be wasteful to have extra space then.
In viewDidLoad I tried this:
- (void)viewDidLoad
{
[super viewDidLoad];
self.leftButton.titleLabel.preferredMaxLayoutWidth = (self.view.frame.size.width - 20.0 * 3) / 2.0;
self.rightButton.titleLabel.preferredMaxLayoutWidth = (self.view.frame.size.width - 20.0 * 3) / 2.0;
}
But that did not change anyhting at all.
The question: How do I make autolayout respect that the left button needs two lines?
I had the same problem where I wanted my button to grow along with its title. I had to sublcass the UIButton and its intrinsicContentSize so that it returns the intrinsic size of the label.
- (CGSize)intrinsicContentSize
{
return self.titleLabel.intrinsicContentSize;
}
Since the UILabel is multiline, its intrinsicContentSize is unknown and you have to set its preferredMaxLayoutWidth See objc.io article about that
- (void)layoutSubviews
{
[super layoutSubviews];
self.titleLabel.preferredMaxLayoutWidth = self.titleLabel.frame.size.width;
[super layoutSubviews];
}
The rest of the layout should work. If you set your both button having equal heights, the other one will grow to. The complete button looks like this
#implementation TAButton
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
self.titleLabel.numberOfLines = 0;
self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
}
return self;
}
- (CGSize)intrinsicContentSize
{
return self.titleLabel.intrinsicContentSize;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.titleLabel.preferredMaxLayoutWidth = self.titleLabel.frame.size.width;
[super layoutSubviews];
}
#end
Swift 4.1.2 Version based on #Jan answer.
import UIKit
class MultiLineButton: UIButton {
// MARK: - Init
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
private func commonInit() {
self.titleLabel?.numberOfLines = 0
self.titleLabel?.lineBreakMode = .byWordWrapping
}
// MARK: - Overrides
override var intrinsicContentSize: CGSize {
get {
return titleLabel?.intrinsicContentSize ?? CGSize.zero
}
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = titleLabel?.frame.size.width ?? 0
super.layoutSubviews()
}
}
A simple solution working for me: make the multiline button to respect its title height in Swift 4.2 by adding a constraint for the button's height based on its title label's height:
let height = NSLayoutConstraint(item: multilineButton,
attribute: .height,
relatedBy: .equal,
toItem: multilineButton.titleLabel,
attribute: .height,
multiplier: 1,
constant: 0)
multilineButton.addConstraint(height)
This respects content edge insets and worked for me:
class MultilineButton: UIButton {
func setup() {
self.titleLabel?.numberOfLines = 0
self.setContentHuggingPriority(UILayoutPriorityDefaultLow + 1, for: .vertical)
self.setContentHuggingPriority(UILayoutPriorityDefaultLow + 1, for: .horizontal)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
override var intrinsicContentSize: CGSize {
let size = self.titleLabel!.intrinsicContentSize
return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
}
}
add the missing constraints:
if let label = button.titleLabel {
button.addConstraint(NSLayoutConstraint(item: label, attribute: .top, relatedBy: .equal, toItem: button, attribute: .top, multiplier: 1.0, constant: 0.0))
button.addConstraint(NSLayoutConstraint(item: label, attribute: .bottom, relatedBy: .equal, toItem: button, attribute: .bottom, multiplier: 1.0, constant: 0.0))
}
Complete class in Swift 3 - based on #Jan, #Quantaliinuxite and #matt bezark:
#IBDesignable
class MultiLineButton:UIButton {
//MARK: -
//MARK: Setup
func setup () {
self.titleLabel?.numberOfLines = 0
//The next two lines are essential in making sure autolayout sizes us correctly
self.setContentHuggingPriority(UILayoutPriorityDefaultLow+1, for: .vertical)
self.setContentHuggingPriority(UILayoutPriorityDefaultLow+1, for: .horizontal)
}
//MARK:-
//MARK: Method overrides
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
override var intrinsicContentSize: CGSize {
return self.titleLabel!.intrinsicContentSize
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
}
}
There is a solution without subclassing on iOS11. Just need to set one additional constraint in code to match height of button and button.titleLabel.
ObjC:
// In init or overriden updateConstraints method
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self.button
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.button.titleLabel
attribute:NSLayoutAttributeHeight
multiplier:1
constant:0];
[self addConstraint:constraint];
And in some cases (as said before):
- (void)layoutSubviews {
[super layoutSubviews];
self.button.titleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.button.titleLabel.frame);
}
Swift:
let constraint = NSLayoutConstraint(item: button,
attribute: .height,
relatedBy: .equal,
toItem: button.titleLabel,
attribute: .height,
multiplier: 1,
constant: 0)
self.addConstraint(constraint)
+
override func layoutSubviews() {
super.layoutSubviews()
button.titleLabel.preferredMaxLayoutWidth = button.titleLabel.frame.width
}
Lot of answers here, but the simple one by #Yevheniia Zelenska worked fine for me. Simplified Swift 5 version:
#IBOutlet private weak var button: UIButton! {
didSet {
guard let titleHeightAnchor = button.titleLabel?.heightAnchor else { return }
button.heightAnchor.constraint(equalTo: titleHeightAnchor).isActive = true
}
}
None of the other answers had everything working for me. Here's my answer:
class MultilineButton: UIButton {
func setup() {
titleLabel?.textAlignment = .center
titleLabel?.numberOfLines = 0
titleLabel?.lineBreakMode = .byWordWrapping
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
override var intrinsicContentSize: CGSize {
var titleContentSize = titleLabel?.intrinsicContentSize ?? CGSize.zero
titleContentSize.height += contentEdgeInsets.top + contentEdgeInsets.bottom
titleContentSize.width += contentEdgeInsets.left + contentEdgeInsets.right
return titleContentSize
}
override func layoutSubviews() {
titleLabel?.preferredMaxLayoutWidth = 300 // Or whatever your maximum is
super.layoutSubviews()
}
}
This won't cater for an image, however.
Have you tried using this:
self.leftButton.titleLabel.textAlignment = NSTextAlignmentCenter;
self.leftButton.titleLabel.lineBreakMode = NSLineBreakByWordWrapping | NSLineBreakByTruncatingTail;
self.leftButton.titleLabel.numberOfLines = 0;
UPDATED Swift/Swift 2.0 version again based on #Jan's answer
#IBDesignable
class MultiLineButton:UIButton {
//MARK: -
//MARK: Setup
func setup () {
self.titleLabel?.numberOfLines = 0
//The next two lines are essential in making sure autolayout sizes us correctly
self.setContentHuggingPriority(UILayoutPriorityDefaultLow+1, forAxis: .Vertical)
self.setContentHuggingPriority(UILayoutPriorityDefaultLow+1, forAxis: .Horizontal)
}
//MARK:-
//MARK: Method overrides
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
override func intrinsicContentSize() -> CGSize {
return self.titleLabel!.intrinsicContentSize()
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
}
}
tweaks for Swift 3.1
intrisicContentSize is a property instead of a function
override var intrinsicContentSize: CGSize {
return self.titleLabel!.intrinsicContentSize
}
Version, which also taking into account titleEdgeInsets and not overrides standard button behaviour unless titleLabel?.numberOfLines set to zero and button image set to nil.
open class Button: UIButton {
override open var intrinsicContentSize: CGSize {
if let titleLabel = titleLabel, titleLabel.numberOfLines == 0, image == nil {
let size = titleLabel.intrinsicContentSize
let result = CGSize(width: size.width + contentEdgeInsets.horizontal + titleEdgeInsets.horizontal,
height: size.height + contentEdgeInsets.vertical + titleEdgeInsets.vertical)
return result
} else {
return super.intrinsicContentSize
}
}
override open func layoutSubviews() {
super.layoutSubviews()
if let titleLabel = titleLabel, titleLabel.numberOfLines == 0, image == nil {
let priority = UILayoutPriority.defaultLow + 1
if titleLabel.horizontalContentHuggingPriority != priority {
titleLabel.horizontalContentHuggingPriority = priority
}
if titleLabel.verticalContentHuggingPriority != priority {
titleLabel.verticalContentHuggingPriority = priority
}
let rect = titleRect(forContentRect: contentRect(forBounds: bounds))
titleLabel.preferredMaxLayoutWidth = rect.size.width
super.layoutSubviews()
}
}
}
I could not find a proper answer that took all these into account:
Use AutoLayout only (meaning no override of layoutSubviews)
Respect the button's contentEdgeInsets
Minimalist (no playing with buttons's intrinsicContentSize)
So here's my take on it, which respects all three points from above.
final class MultilineButton: UIButton {
/// Buttons don't have built-in layout support for multiline labels.
/// This constraint is here to provide proper button's height given titleLabel's height and contentEdgeInset.
private var heightCorrectionConstraint: NSLayoutConstraint?
override var contentEdgeInsets: UIEdgeInsets {
didSet {
heightCorrectionConstraint?.constant = -(contentEdgeInsets.top + contentEdgeInsets.bottom)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupLayout()
}
private func setupLayout() {
titleLabel?.numberOfLines = 0
heightCorrectionConstraint = titleLabel?.heightAnchor.constraint(equalTo: heightAnchor, constant: 0)
heightCorrectionConstraint?.priority = .defaultHigh
heightCorrectionConstraint?.isActive = true
}
}
Note
I did not modify the button's intrinsicContentSize, there is no need to play with it. When the label is 2+ lines, the button's natural intrinsicContentSize's height is smaller than the desired height. The constraint that I added (heightCorrectionConstraint) corrects that automatically. Just make sure that the button's contentHuggingPriority in the vertical axis is smaller than the heightCorrectionConstraint's priority (which is the default).
#Jan's answer doesn't work for me in (at least) iOS 8.1, 9.0 with Xcode 9.1. The problem: titleLabel's -intrinsicContentSize returns very big width and small height as there is no width limit at all (titleLabel.frame on call has zero size that leads to measurements problem). Moreover, it doesn't take into account possible insets and/or image.
So, here is my implementation that should fix all the stuff (only one method is really necessary):
#implementation PRButton
- (CGSize)intrinsicContentSize
{
CGRect titleFrameMax = UIEdgeInsetsInsetRect(UIEdgeInsetsInsetRect(UIEdgeInsetsInsetRect(
self.bounds, self.alignmentRectInsets), self.contentEdgeInsets), self.titleEdgeInsets
);
CGSize titleSize = [self.titleLabel sizeThatFits:CGSizeMake(titleFrameMax.size.width, CGFLOAT_MAX)];
CGSize superSize = [super intrinsicContentSize];
return CGSizeMake(
titleSize.width + (self.bounds.size.width - titleFrameMax.size.width),
MAX(superSize.height, titleSize.height + (self.bounds.size.height - titleFrameMax.size.height))
);
}
#end
//Swift 4 - Create Dynamic Button MultiLine Dynamic
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
/// Add DemoButton 1
let demoButton1 = buildButton("Demo 1")
//demoButton1.addTarget(self, action: #selector(ViewController.onDemo1Tapped), for: .touchUpInside)
view.addSubview(demoButton1)
view.addConstraint(NSLayoutConstraint(item: demoButton1, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: demoButton1, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: -180))
}
func buildButton(_ title: String) -> UIButton {
let button = UIButton(type: .system)
button.backgroundColor = UIColor(red: 80/255, green: 70/255, blue: 66/255, alpha: 1.0)
//--------------------------
//to make the button multiline
//button.titleLabel!.lineBreakMode = .byWordWrapping
button.titleLabel?.textAlignment = .center
button.titleLabel?.numberOfLines = 0
//button.titleLabel?.adjustsFontSizeToFitWidth = true
//button.sizeToFit()
button.titleLabel?.preferredMaxLayoutWidth = self.view.bounds.width//200
button.layer.borderWidth = 2
let height = NSLayoutConstraint(item: button,
attribute: .height,
relatedBy: .equal,
toItem: button.titleLabel,
attribute: .height,
multiplier: 1,
constant: 0)
button.addConstraint(height)
//--------------------------
button.setTitle(title, for: UIControlState())
button.layer.cornerRadius = 4.0
button.setTitleColor(UIColor(red: 233/255, green: 205/255, blue: 193/255, alpha: 1.0), for: UIControlState())
button.translatesAutoresizingMaskIntoConstraints = false
return button
}
}
Instead of calling layoutSubviews twice I'd calculate preferredMaxLayoutWidth manually
#objcMembers class MultilineButton: UIButton {
override var intrinsicContentSize: CGSize {
// override to have the right height with autolayout
get {
var titleContentSize = titleLabel!.intrinsicContentSize
titleContentSize.height += contentEdgeInsets.top + contentEdgeInsets.bottom
return titleContentSize
}
}
override func awakeFromNib() {
super.awakeFromNib()
titleLabel!.numberOfLines = 0
}
override func layoutSubviews() {
let contentWidth = width - contentEdgeInsets.left - contentEdgeInsets.right
let imageWidth = imageView?.width ?? 0 + imageEdgeInsets.left + imageEdgeInsets.right
let titleMaxWidth = contentWidth - imageWidth - titleEdgeInsets.left - titleEdgeInsets.right
titleLabel!.preferredMaxLayoutWidth = titleMaxWidth
super.layoutSubviews()
}
}

Resources