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()
}
}
Related
I have a file for the View section of app, where i have all the labels and images that i intent to use, this is what i have in my DetailViewTableCell class, which inherits from UIView.
class DetailViewTableCell: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
}
Now i move to my DetailViewController class, here i try and add the label, the label is added but it appears always at top left corner at 0,0 coordinate, when i try and add constraints for position, i always get error, now i can try
detailMain.detailName.frame.origin.x = 30
but i get error:
Constraint items must each be a view or layout guide.
In any case i do not wish to use this approach but more something like this
NSLayoutConstraint(item: detailMain.detailName, attribute: .leading, relatedBy: .equal, toItem: detailMain.detailName.superview, attribute: .leading, multiplier: 1, constant: 20).isActive = true
but i get the same above error, my over all code is this:
self.view.addSubview(detailMain.detailName)
detailMain.detailName.translatesAutoresizingMaskIntoConstraints = false
detailMain.detailName.heightAnchor.constraint(equalToConstant: 25).isActive = true
detailMain.detailName.widthAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true
detailMain.detailName.font = UIFont(name: "Rubik-Medium", size: 30)
detailMain.detailName.backgroundColor = UIColor.white
detailMain.detailName.textColor = UIColor.black
Which works perfectly fine but the moment i try and constraints, the error come up, this is how the app shows up with out constraints and name at top most left corner
////////UPDATE
So here is my new DetailViewTableCell,
import UIKit
class DetailViewTableCell: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
[detailMainImage, detailName, detailType, detailHeart].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
NSLayoutConstraint.activate([
// constrain main image to all 4 sides
detailMainImage.topAnchor.constraint(equalTo: topAnchor),
detailMainImage.leadingAnchor.constraint(equalTo: leadingAnchor),
detailMainImage.trailingAnchor.constraint(equalTo: trailingAnchor),
detailMainImage.bottomAnchor.constraint(equalTo: bottomAnchor),
// activate the height contraint
// constrain detailType label
// 30-pts from Leading
// 12-pts from Top
detailType.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailType.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
// constrain detailName label
// 30-pts from Leading
// 12-pts from Bottom
detailName.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
// constrain detailHeart image
// 12-pts from Trailing
// 12-pts from Bottom
// width: 24 height: equal to width (1:1 square)
detailHeart.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
detailHeart.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
detailHeart.widthAnchor.constraint(equalToConstant: 24),
detailHeart.heightAnchor.constraint(equalTo: detailHeart.widthAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and this 2 lines is what i add to my Detail view controller in viewDidLoad
let v = DetailViewTableCell()
detailTableView.tableHeaderView = v
Also i add this function
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = detailTableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
detailTableView.tableHeaderView = headerView
detailTableView.layoutIfNeeded()
}
}
then in my viewForHeaderInSection inbuilt function i add this
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
tableView.rowHeight = 80
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
headerView.heightAnchor.constraint(equalToConstant: 400).isActive = true
detailMain.detailMainImage.translatesAutoresizingMaskIntoConstraints = false
detailMain.detailMainImage.heightAnchor.constraint(equalToConstant: 400).isActive = true
detailMain.detailMainImage.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
detailMain.detailMainImage.image = UIImage(named: restaurant.image)
detailMain.detailMainImage.contentMode = .scaleAspectFill
detailMain.detailMainImage.clipsToBounds = true
headerView.addSubview(detailMain.detailMainImage)
//Add the name
detailMain.detailName.text = restaurant.name
headerView.addSubview(detailMain.detailName)
return headerView
}
but still same position , is there any thing i add to add or remove from my code
You didn't show where you *want the labels, but this should get you going...
In your "header view" class:
add your elements: detailMainImage, detailName, etc...
set their properties and constraints as desired
You can get auto-layout to use the constraints you've setup in the header view to automatically determine its height:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = tableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
tableView.tableHeaderView = headerView
tableView.layoutIfNeeded()
}
}
So, here's a complete example:
class TestHeaderTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// instantiate the header view
let v = DetailTableHeaderView()
// set it as the tableHeaderView
tableView.tableHeaderView = v
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = tableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
tableView.tableHeaderView = headerView
tableView.layoutIfNeeded()
}
}
}
class DetailTableHeaderView: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .white
// give the heart image view a background color so we can see its frame
detailHeart.backgroundColor = .red
if let img = UIImage(named: "teacup") {
detailMainImage.image = img
}
// give labels some text so we can see them
detailType.text = "Detail Type"
detailName.text = "Detail Name"
// setup fonts for labels as desired
//detailName.font = UIFont(name: "Rubik-Medium", size: 30)
// I don't have "Rubik" so this is with the system font
detailName.font = UIFont.systemFont(ofSize: 30, weight: .bold)
detailName.backgroundColor = UIColor.white
detailName.textColor = UIColor.black
detailType.textColor = .white
[detailMainImage, detailName, detailType, detailHeart].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
// give main image a height
// set its Priority to 999 to prevent layout constraint conflict warnings
let mainImageHeightAnchor = detailMainImage.heightAnchor.constraint(equalToConstant: 300.0)
mainImageHeightAnchor.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
// constrain main image to all 4 sides
detailMainImage.topAnchor.constraint(equalTo: topAnchor),
detailMainImage.leadingAnchor.constraint(equalTo: leadingAnchor),
detailMainImage.trailingAnchor.constraint(equalTo: trailingAnchor),
detailMainImage.bottomAnchor.constraint(equalTo: bottomAnchor),
// activate the height contraint
mainImageHeightAnchor,
// constrain detailType label
// 30-pts from Leading
// 12-pts from Top
detailType.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailType.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
// constrain detailName label
// 30-pts from Leading
// 12-pts from Bottom
detailName.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
// constrain detailHeart image
// 12-pts from Trailing
// 12-pts from Bottom
// width: 24 height: equal to width (1:1 square)
detailHeart.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
detailHeart.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
detailHeart.widthAnchor.constraint(equalToConstant: 24),
detailHeart.heightAnchor.constraint(equalTo: detailHeart.widthAnchor),
])
}
}
For this example, I just set the background color of the "heart" image view to red, and I clipped the teacup out of your image:
And this is the result:
Edit - to use the custom view as a Section header view...
Use the same DetailTableHeaderView class from above, but change the table view controller as follows:
class TestSectionHeaderTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.sectionHeaderHeight = UITableView.automaticDimension
tableView.estimatedSectionHeaderHeight = 300
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let v = DetailTableHeaderView()
// for example implementation...
if let img = UIImage(named: "teacup") {
v.detailMainImage.image = img
}
v.detailName.text = "Testing the Name"
// for your implementation...
//if let img = UIImage(named: restaurant.image) {
// v.detailMainImage.image = img
//}
//v.detailName.text = restaurant.name
return v
}
return nil;
}
}
I have created a simple UIView that contains a red box (UIImage) at its centre. When all the constraints are constants the code works fine. However, if I replace the height constraint with one that makes the box half the height of the view then the box disappears.
I assume that this is either because I am doing it wrong (obviously) or I need to do something more to force the constraint to realise the UIView height is greater than zero.
How do I set the redBox height constraint so that it is always half the height of the BoxView?
import UIKit
class BoxView: UIView {
public var redBox: UIImageView
public override init(frame: CGRect) {
redBox = UIImageView(frame: .zero)
redBox.backgroundColor = .red
super.init(frame: frame)
self.backgroundColor = .yellow
addSubview(redBox)
redBox.translatesAutoresizingMaskIntoConstraints = false
let margins = layoutMarginsGuide
redBox.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
redBox.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
redBox.widthAnchor.constraint(equalToConstant: 100).isActive = true
//redBox.heightAnchor.constraint(equalToConstant: 100).isActive = true
redBox.heightAnchor.constraint(equalTo: self.heightAnchor, constant: 0.5)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view = BoxView()
}
}
Replace
redBox.heightAnchor.constraint(equalTo: self.heightAnchor, constant: 0.5)
with
redBox.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.5).isActive = true
NSLayoutConstraint.activate([
redBox.centerXAnchor.constraint(equalTo: self.centerXAnchor),
redBox.centerYAnchor.constraint(equalTo: self.centerYAnchor),
redBox.widthAnchor.constraint(equalToConstant: 100),
redBox.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.5)
])
In your current code first you miss the .isActive = true which has the same effect as if the line doesn't exist , and if specified this will make the box height equal to the view's height + constant ( = 0.5 )
box height = view height * multiplier + constant
and since default multiplier = 1 and you set constant = 0.5 this will be
box height = view height * 1.0 + 0.5
But instead you need
box height = view height * 0.5 + 0 // omit consatnt in constraint and it will be zero
class BoxView: UIView {
public var redBox: UIImageView
public override init(frame: CGRect) {
super.init(frame: frame)
redBox = UIImageView(frame: .zero)
redBox.backgroundColor = .red
self.backgroundColor = .yellow
addSubview(redBox)
redBox.translatesAutoresizingMaskIntoConstraints = false
let margins = layoutMarginsGuide
NSLayoutConstraint.activate([
redBox.centerXAnchor.constraint(equalTo: self.centerXAnchor),
redBox.centerYAnchor.constraint(equalTo: self.centerYAnchor),
redBox.widthAnchor.constraint(equalToConstant: 100),
redBox.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.5)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
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
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
}
I want to subclass UIView and show a login like view. I've created this in Objective-C, but now I want to port it to Swift. I do not use storyboards, so I create all my UI in code.
But the first problem is that I must implement initWithCoder. I gave it a default implementation since It won't be called. Now when I run the program it crashes, because I've to implement initWithFrame as well. Now I got this:
override init() {
super.init()
println("Default init")
}
override init(frame: CGRect) {
super.init(frame: frame)
println("Frame init")
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
println("Coder init")
}
My question is where should I create my textfield etc... and if I never implement frame and coder how can I "hide" this?
I usually do something like this, its a bit verbose.
class MyView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
addBehavior()
}
convenience init() {
self.init(frame: CGRect.zero)
}
required init(coder aDecoder: NSCoder) {
fatalError("This class does not support NSCoding")
}
func addBehavior() {
print("Add all the behavior here")
}
}
let u = MyView(frame: CGRect.zero)
let v = MyView()
(Edit: I've edited my answer so that the relation between the initializers is more clear)
This is more simple.
override init (frame : CGRect) {
super.init(frame : frame)
// Do what you want.
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
Custom UIView Subclass Example
I usually create iOS apps without using storyboards or nibs. I'll share some techniques I've learned to answer your questions.
Hiding Unwanted init Methods
My first suggestion is to declare a base UIView to hide unwanted initializers. I've discussed this approach in detail in my answer to "How to Hide Storyboard and Nib Specific Initializers in UI Subclasses". Note: This approach assumes you will not use BaseView or its descendants in storyboards or nibs since it will intentionally cause the app to crash.
class BaseView: UIView {
// This initializer hides init(frame:) from subclasses
init() {
super.init(frame: CGRect.zero)
}
// This attribute hides `init(coder:)` from subclasses
#available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported")
}
}
Your custom UIView subclass should inherit from BaseView. It must call super.init() in its initializer. It does not need to implement init(coder:). This is demonstrated in the example below.
Adding a UITextField
I create stored properties for subviews referenced outside of the init method. I would typically do so for a UITextField. I prefer to instantiate subviews within the declaration of the subview property like this: let textField = UITextField().
The UITextField will not be visible unless you add it to the custom view's subview list by calling addSubview(_:). This is demonstrated in the example below.
Programmatic Layout Without Auto Layout
The UITextField will not be visible unless you set its size and position. I often do layout in code (not using Auto Layout) within the layoutSubviews method. layoutSubviews() is called initially and whenever a resize event happens. This allows adjusting layout depending on the size of CustomView. For example, if CustomView appears the full width on various sizes of iPhones and iPads and adjusts for rotation, it needs to accommodate many initial sizes and resize dynamically.
You can refer to frame.height and frame.width within layoutSubviews() to get the CustomView's dimensions for reference. This is demonstrated in the example below.
Example UIView Subclass
A custom UIView subclass containing a UITextField which does not need to implement init?(coder:).
class CustomView: BaseView {
let textField = UITextField()
override init() {
super.init()
// configure and add textField as subview
textField.placeholder = "placeholder text"
textField.font = UIFont.systemFont(ofSize: 12)
addSubview(textField)
}
override func layoutSubviews() {
super.layoutSubviews()
// Set textField size and position
textField.frame.size = CGSize(width: frame.width - 20, height: 30)
textField.frame.origin = CGPoint(x: 10, y: 10)
}
}
Programmatic Layout with Auto Layout
You can also implement layout using Auto Layout in code. Since I don't often do this, I will not show an example. You can find examples of implementing Auto Layout in code on Stack Overflow and elsewhere on the Internet.
Programmatic Layout Frameworks
There are open source frameworks that implement layout in code. One I am interested in but have not tried is LayoutKit. It was written by the development team an LinkedIn. From the Github repository: "LinkedIn created LayoutKit because we have found that Auto Layout is not performant enough for complicated view hierarchies in scrollable views."
Why put fatalError in init(coder:)
When creating UIView subclasses that will never be used in a storyboard or nib, you might introduce initializers with different parameters and initialization requirements that could not be called by the init(coder:) method. If you did not fail init(coder:) with a fatalError, it could lead to very confusing problems down the line if accidentally used in a storyboard/nib. The fatalError asserts these intentions.
required init?(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported")
}
If you want to run some code when the subclass is created regardless of whether it is created in code or a storyboard/nib then you could do something like the following (based on Jeff Gu Kang’s answer)
class CustomView: UIView {
override init (frame: CGRect) {
super.init(frame: frame)
initCommon()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initCommon()
}
func initCommon() {
// Your custom initialization code
}
}
It's important that your UIView can be created by interface builder/storyboards or from code. I find it's useful to have a setup method to reduce duplicating any setup code. e.g.
class RedView: UIView {
override init (frame: CGRect) {
super.init(frame: frame)
setup()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
setup()
}
func setup () {
backgroundColor = .red
}
}
Swift 4.0,If you want to use view from xib file, then this is for you. I created CustomCalloutView class Sub class of UIView. I have created a xib file and in IB just select file owner then select Attribute inspector set class name to CustomCalloutView, then create outlet in your class.
import UIKit
class CustomCalloutView: UIView {
#IBOutlet var viewCallout: UIView! // This is main view
#IBOutlet weak var btnCall: UIButton! // subview of viewCallout
#IBOutlet weak var btnDirection: UIButton! // subview of viewCallout
#IBOutlet weak var btnFavourite: UIButton! // subview of viewCallout
// let nibName = "CustomCalloutView" this is name of xib file
override init(frame: CGRect) {
super.init(frame: frame)
nibSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
nibSetup()
}
func nibSetup() {
Bundle.main.loadNibNamed(String(describing: CustomCalloutView.self), owner: self, options: nil)
guard let contentView = viewCallout else { return } // adding main view
contentView.frame = self.bounds //Comment this line it take default frame of nib view
// custom your view properties here
self.addSubview(contentView)
}
}
// Now adding it
let viewCustom = CustomCalloutView.init(frame: CGRect.init(x: 120, y: 120, 50, height: 50))
self.view.addSubview(viewCustom)
Here's an example of how I usually build my subclasses(UIView). I have the content as variables so they can be accessed and tweaked maybe later in some other class. I've also shown how I use auto layout and adding content.
For example in a ViewController I have this view initialized In ViewDidLoad() since that is only called once when the view is visible. Then I use these functions I make here addContentToView() and then activateConstraints() to build the content and set constraints. If I later in a ViewController want the color of let's say a button to be red, I just do that in that specific function in that ViewController.
Something like: func tweaksome(){ self.customView.someButton.color = UIColor.red}
class SomeView: UIView {
var leading: NSLayoutConstraint!
var trailing: NSLayoutConstraint!
var bottom: NSLayoutConstraint!
var height: NSLayoutConstraint!
var someButton: UIButton = {
var btn: UIButton = UIButton(type: UIButtonType.system)
btn.setImage(UIImage(named: "someImage"), for: .normal)
btn.translatesAutoresizingMaskIntoConstraints = false
return btn
}()
var btnLeading: NSLayoutConstraint!
var btnBottom: NSLayoutConstraint!
var btnTop: NSLayoutConstraint!
var btnWidth: NSLayoutConstraint!
var textfield: UITextField = {
var tf: UITextField = UITextField()
tf.adjustsFontSizeToFitWidth = true
tf.placeholder = "Cool placeholder"
tf.translatesAutoresizingMaskIntoConstraints = false
tf.backgroundColor = UIColor.white
tf.textColor = UIColor.black
return tf
}()
var txtfieldLeading: NSLayoutConstraint!
var txtfieldTrailing: NSLayoutConstraint!
var txtfieldCenterY: NSLayoutConstraint!
override init(frame: CGRect){
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//fatalError("init(coder:) has not been implemented")
}
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
func activateConstraints(){
NSLayoutConstraint.activate([self.btnLeading, self.btnBottom, self.btnTop, self.btnWidth])
NSLayoutConstraint.activate([self.txtfieldCenterY, self.txtfieldLeading, self.txtfieldTrailing])
}
func addContentToView(){
//setting the sizes
self.addSubview(self.userLocationBtn)
self.btnLeading = NSLayoutConstraint(
item: someButton,
attribute: .leading,
relatedBy: .equal,
toItem: self,
attribute: .leading,
multiplier: 1.0,
constant: 5.0)
self.btnBottom = NSLayoutConstraint(
item: someButton,
attribute: .bottom,
relatedBy: .equal,
toItem: self,
attribute: .bottom,
multiplier: 1.0,
constant: 0.0)
self.btnTop = NSLayoutConstraint(
item: someButton,
attribute: .top,
relatedBy: .equal,
toItem: self,
attribute: .top,
multiplier: 1.0,
constant: 0.0)
self.btnWidth = NSLayoutConstraint(
item: someButton,
attribute: .width,
relatedBy: .equal,
toItem: self,
attribute: .height,
multiplier: 1.0,
constant: 0.0)
self.addSubview(self.textfield)
self.txtfieldLeading = NSLayoutConstraint(
item: self.textfield,
attribute: .leading,
relatedBy: .equal,
toItem: someButton,
attribute: .trailing,
multiplier: 1.0,
constant: 5)
self.txtfieldTrailing = NSLayoutConstraint(
item: self.textfield,
attribute: .trailing,
relatedBy: .equal,
toItem: self.doneButton,
attribute: .leading,
multiplier: 1.0,
constant: -5)
self.txtfieldCenterY = NSLayoutConstraint(
item: self.textfield,
attribute: .centerY,
relatedBy: .equal,
toItem: self,
attribute: .centerY,
multiplier: 1.0,
constant: 0.0)
}
}