3 views next to each other programmatically constraints - ios

I'm trying to create a custom callOutView and for this i have a bubbleView, which is a subclass of a UIView in this view i want to create 3 views next to eachother. First an imageView, which has an static width and height on 60. Then an UIView which has a dynamically width depending on the total width of the bubbleView. Then in the end there is another imageView with an static height and width again at 60. How can i achieve this? i've tried below with snapKit, but does not seem to work.
Illustration of what i want
Code i've tried
bubbleView = BubbleView()
bubbleView?.clipsToBounds = true
bubbleView?.layer.masksToBounds = true
self.addSubview(bubbleView!)
let logoImageView = UIImageView()
logoImageView.contentMode = UIViewContentMode.ScaleAspectFill
logoImageView.image = UIImage(data: logoImage!)
bubbleView?.contentView.addSubview(logoImageView)
logoImageView.backgroundColor = UIColor.whiteColor()
let informationView = UIView()
bubbleView?.contentView.addSubview(informationView)
informationView.backgroundColor = UIColor.redColor()
let discView = UIImageView()
discView.contentMode = UIViewContentMode.ScaleAspectFill
discView.image = UIImage(data: logoImage!)
bubbleView?.contentView.addSubview(discView)
discView.backgroundColor = UIColor.whiteColor()
logoImageView.snp_makeConstraints { (make) -> Void in
make.top.equalTo(bubbleView!).offset(0)
make.left.equalTo(bubbleView!).offset(0)
make.height.equalTo(60)
make.right.equalTo(informationView)
}
informationView.snp_makeConstraints { (make) -> Void in
make.top.equalTo(bubbleView!).offset(0)
make.left.equalTo(logoImageView).offset(0)
make.height.equalTo(60)
make.right.equalTo(discView).offset(0)
}
discView.snp_makeConstraints { (make) -> Void in
make.top.equalTo(bubbleView!).offset(0)
make.left.equalTo(informationView).offset(0)
make.height.equalTo(60)
make.right.equalTo(bubbleView!)
}

You need to set
width constraint on leftView and rightView equal to 60.
middleView.leading equal leftView.trailing
middleView.trailing equal rightView.leading.
all.height equal to 60.
all.top equal parent.top
You can try this in Playground.
import UIKit
import XCPlayground
let parentView = UIView()
parentView.frame.size = CGSize(width: 450, height: 60)
parentView.backgroundColor = UIColor.whiteColor()
let leftView = UIView()
leftView.translatesAutoresizingMaskIntoConstraints = false
leftView.backgroundColor = .blackColor()
let rightView = UIView()
rightView.translatesAutoresizingMaskIntoConstraints = false
rightView.backgroundColor = .grayColor()
let middleView = UIView()
middleView.translatesAutoresizingMaskIntoConstraints = false
middleView.backgroundColor = .lightGrayColor()
// add subview
parentView.addSubview(leftView)
parentView.addSubview(middleView)
parentView.addSubview(rightView)
// config constraints
leftView.leadingAnchor.constraintEqualToAnchor(parentView.leadingAnchor).active = true
leftView.topAnchor.constraintEqualToAnchor(parentView.topAnchor).active = true
leftView.heightAnchor.constraintEqualToConstant(60).active = true
leftView.widthAnchor.constraintEqualToConstant(60).active = true
rightView.trailingAnchor.constraintEqualToAnchor(parentView.trailingAnchor).active = true
rightView.topAnchor.constraintEqualToAnchor(parentView.topAnchor).active = true
rightView.heightAnchor.constraintEqualToConstant(60).active = true
rightView.widthAnchor.constraintEqualToConstant(60).active = true
middleView.leadingAnchor.constraintEqualToAnchor(leftView.trailingAnchor).active = true
middleView.trailingAnchor.constraintEqualToAnchor(rightView.trailingAnchor).active = true
middleView.topAnchor.constraintEqualToAnchor(parentView.topAnchor).active = true
middleView.bottomAnchor.constraintEqualToAnchor(parentView.bottomAnchor).active = true
XCPlaygroundPage.currentPage.liveView = parentView

try the following:
let bubbleView = UIView()
bubbleView.translatesAutoresizingMaskIntoConstraints = false
let logoImageView = UIImageView()
logoImageView.translatesAutoresizingMaskIntoConstraints = false
logoImageView.backgroundColor = .darkGrayColor()
bubbleView.addSubview(logoImageView)
let informationView = UIView()
informationView.translatesAutoresizingMaskIntoConstraints = false
informationView.backgroundColor = .lightGrayColor()
bubbleView.addSubview(informationView)
let discImageView = UIImageView()
discImageView.translatesAutoresizingMaskIntoConstraints = false
discImageView.backgroundColor = .darkGrayColor()
bubbleView.addSubview(discImageView)
let views = ["logoImageView": logoImageView, "informationView": informationView, "discImageView": discImageView]
bubbleView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[logoImageView(60)][informationView][discImageView(60)]|", options: [.AlignAllBottom, .AlignAllTop], metrics: nil, views: views))
bubbleView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[logoImageView(60)]|", options: [], metrics: nil, views: views))
// helper constraint so that the information view is at least two times the imageviews' width
bubbleView.addConstraint(NSLayoutConstraint(item: informationView, attribute: .Width, relatedBy: .GreaterThanOrEqual, toItem: logoImageView, attribute: .Width, multiplier: 2.0, constant: 0.0))
view.addSubview(bubbleView)
view.addConstraint(NSLayoutConstraint(item: bubbleView, attribute: .CenterX, relatedBy: .Equal, toItem: view, attribute: .CenterX, multiplier: 1.0, constant: 0.0))
view.addConstraint(NSLayoutConstraint(item: bubbleView, attribute: .CenterY, relatedBy: .Equal, toItem: view, attribute: .CenterY, multiplier: 1.0, constant: 0.0))
result:

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:

Empty UITableView with tableFooterView

I have a UITableView in which I want to show a message when the dataSource is empty. I do this with the well-known method of setting the backgroundView using the following extension:
extension UITableView {
func setEmptyMessage(_ message: String, _ image: String) {
let emptyView: UIView = {
let emptyView = UIView(frame: CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height))
return emptyView
}()
let contentView: UIView = {
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
return contentView
}()
let messageLabel = UILabel()
let messageCommentStyle = NSMutableParagraphStyle()
messageCommentStyle.lineHeightMultiple = 1.2
let attributedString = NSMutableAttributedString(string: message)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: lightFeedUserNameFontColor, range: NSRange(location: 0, length: attributedString.length))
attributedString.addAttribute(NSAttributedString.Key.paragraphStyle, value: messageCommentStyle, range: NSRange(location: 0, length: attributedString.length))
attributedString.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: normalFontSize), range: NSRange(location: 0, length: attributedString.length))
messageLabel.attributedText = attributedString
messageLabel.numberOfLines = 0
messageLabel.font = UIFont.systemFont(ofSize: normalFontSize)
messageLabel.textAlignment = .center
messageLabel.sizeToFit()
messageLabel.translatesAutoresizingMaskIntoConstraints = false
let errorImage: UIImageView = {
let errorImage = UIImageView()
errorImage.translatesAutoresizingMaskIntoConstraints = false
return errorImage
}()
self.backgroundView = emptyView
emptyView.addSubview(contentView)
contentView.addSubview(errorImage)
contentView.addSubview(messageLabel)
contentView.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor).isActive = true
contentView.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor).isActive = true
contentView.leadingAnchor.constraint(equalTo: emptyView.leadingAnchor, constant: normalSpacing * 3).isActive = true
contentView.trailingAnchor.constraint(equalTo: emptyView.trailingAnchor, constant: -(normalSpacing * 3)).isActive = true
contentView.topAnchor.constraint(equalTo: errorImage.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: messageLabel.bottomAnchor).isActive = true
messageLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
messageLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
messageLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
}
func restore() {
self.backgroundView = nil
}
}
And I set it like this:
if(tableData.isEmpty) {
self.tableView.setEmptyMessage("No results!", "none")
} else {
self.tableView.restore()
}
self.tableView.reloadData()
No big deal, we all have seen this and probably used it. And it works great. However, I now have a UIButton on the bottom placed in the tableFooterView. This button stays on top of the UITableView because it automatically positions itself right under the last cell, which is precisely what I want when there is data, but now the empty message is shown in the middle of the screen while the button is above it. How can I fix this so there is a sort of frame when the dataSource is empty?
To illustrate:
So now you are setting your tableView backgroundView to your emptyView. You can take a look on your dataSource and if it's empty - just return new cell for empty state with this message.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count > 0 ? dataSource.count : 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if dataSource.count.isEmpty 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "EmptyCell")
cell.textLabel.text = "No Results!"
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "SomeCell")
return cell
}
}
Write an extension
extension UITableView
{
func addErrorMessageLabel(noDataText:String = "No data available")->UILabel
{
let noDatalabel:UILabel!
noDatalabel=UILabel(frame: CGRect(x: self.frame.size.width/2-200, y: self.frame.size.height/2-50, width: 400, height: 100))
noDatalabel.textColor = textThemeColor
noDatalabel.text=noDataText
noDatalabel.numberOfLines=0
noDatalabel.textAlignment = .center
noDatalabel.isHidden=true
self.addSubview(noDatalabel)
self.alignCenterToSuperView(item: noDatalabel, horizentally: true, vertically: true,height: 100,width: 400)
return noDatalabel
}
func alignCenterToSuperView(item:UIView,horizentally:Bool,vertically:Bool , height:Int, width:Int)
{
if horizentally
{
item.translatesAutoresizingMaskIntoConstraints = false
let xConstraint = NSLayoutConstraint(item: item, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([xConstraint])
}
if vertically
{
item.translatesAutoresizingMaskIntoConstraints = false
let yConstraint = NSLayoutConstraint(item: item, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([yConstraint])
}
let Height = NSLayoutConstraint(item: item, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant:CGFloat(height))
item.addConstraint(Height)
let Width = NSLayoutConstraint(item: item, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant:CGFloat(width))
item.addConstraints([Width,Height])
}
}
In your ViewController create
var noDataLabel:UILabel!
In your ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
noDataLabel = tableView.addErrorMessageLabel()
noDataLabel.text = "No data found" // or your message
}
And final step
if(tableData.isEmpty) {
noDataLabel.isHideen = false
} else {
noDataLabel.isHideen = true
}

How do I create UITableView header whose height is determined by the height of its label?

I would like to add a header to my tableView. This header contains 1 UILabel. The header height should be calculated based on the number of lines the label has.
In my code, I'm adding constraints with all the edges of the label <> header. This is my attempt:
//Add header to tableView
header = UIView()
header.backgroundColor = UIColor.yellowColor()
tableView!.tableHeaderView = header
//Create Label and add it to the header
postBody = UILabel()
postBody.text = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog."
postBody.font = UIFont(name: "Lato-Regular", size: 16.0)
postBody.numberOfLines = 0
postBody.backgroundColor = FlatLime()
header.addSubview(postBody)
//Enable constraints for each item
postBody.translatesAutoresizingMaskIntoConstraints = false
header.translatesAutoresizingMaskIntoConstraints = false
//Add constraints to the header and post body
let postBodyLeadingConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Leading, multiplier: 1, constant: 0)
postBodyLeadingConstraint.active = true
let postBodyTrailingConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Trailing, multiplier: 1, constant: 0)
postBodyTrailingConstraint.active = true
let postBodyTopConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 0)
postBodyTopConstraint.active = true
let postBodyBottomConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0)
postBodyBottomConstraint.active = true
//Calculate header size
let size = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
var frame = header.frame
frame.size.height = size.height
header.frame = frame
tableView!.tableHeaderView = header
header.layoutIfNeeded()
This is my table:
let nib = UINib(nibName: "MessagesTableViewCell", bundle: nil)
let nibSimple = UINib(nibName: "SimpleMessagesTableViewCell", bundle: nil)
self.tableView!.registerNib(nib, forCellReuseIdentifier: "MessagesTableViewCell")
self.tableView!.registerNib(nibSimple, forCellReuseIdentifier: "SimpleMessagesTableViewCell")
self.tableView!.dataSource = self
self.tableView!.delegate = self
self.tableView!.rowHeight = UITableViewAutomaticDimension
self.tableView!.estimatedRowHeight = 100.0
self.tableView!.separatorStyle = UITableViewCellSeparatorStyle.None
self.tableView!.separatorColor = UIColor(hex: 0xf5f5f5)
self.tableView!.separatorInset = UIEdgeInsetsMake(0, 0, 0, 0)
self.tableView!.clipsToBounds = true
self.tableView!.allowsSelection = false
self.tableView!.allowsMultipleSelection = false
self.tableView!.keyboardDismissMode = .OnDrag
As you can see, the header does not take into account the height of the label (which I did numberOfLines = 0)
UILabels take advantage of UIView's intrinsicContentSize() to tell auto layout what size they should be. For a multiline label, however, the intrinsic content size is ambiguous; the table doesn't know if it should be short and wide, tall and narrow, or anything in between.
To combat this, UILabel has a property called preferredMaxLayoutWidth. Setting this tells a multiline label that it should be at most this wide, and allows intrinsicContentSize() to figure out and return an appropriate height to match. By not setting the preferredMaxLayoutWidth in your example, the label leaves its width unbounded and therefore calculates the height for a long, single line of text.
The only complication with preferredMaxLayoutWidth is that you typically don't know what width you want the label to be until auto layout has calculated one for you. For that reason, the place to set it in a view controller subclass (which it looks like your code sample might be from) is in viewDidLayoutSubviews:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
postBody.preferredMaxLayoutWidth = CGRectGetWidth(postBody.frame)
// then update the table header view
if let header = tableView?.tableHeaderView {
header.frame.size.height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
tableView?.tableHeaderView = header
}
}
Obviously, you'll need to add a property for the postBody label for this to work.
Let me know if you're not in a UIViewController subclass here and I'll edit my answer.
Implementation using the storyboard
In UItableView add on UITableViewCell new UIView and put him UILabel
Connects them via Autolayout
In UILabel put the number of lines to 0.
In ViewDidLoad your UILabel call a method sizeToFit()
and specify a size for UIView, and that will be your HeaderVew headerView.frame.size.height = headerLabel.frame.size.height
Code
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var headerView: UIView!
#IBOutlet weak var headerLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
headerLabel.text = "tableViewdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarning"
headerLabel.sizeToFit()
headerView.frame.size.height = headerLabel.frame.size.height
}
ScreenShot
TestProject
test project link
The first problem we have is that the header cannot be resized by autolayout, for details, see Is it possible to use AutoLayout with UITableView's tableHeaderView?
Therefore, we have to calculate the height of the header manually, for example:
#IBOutlet var table: UITableView!
var header: UIView?
var postBody: UILabel?
override func viewDidLoad() {
super.viewDidLoad()
let header = UIView()
// don't forget to set this
header.translatesAutoresizingMaskIntoConstraints = true
header.backgroundColor = UIColor.yellowColor()
let postBody = UILabel()
postBody.translatesAutoresizingMaskIntoConstraints = false
postBody.text = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog."
postBody.font = UIFont.systemFontOfSize(16.0)
// don't forget to set this
postBody.lineBreakMode = .ByWordWrapping
postBody.numberOfLines = 0
header.addSubview(postBody)
let leadingConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Leading, multiplier: 1, constant: 0)
let trailingConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Trailing, multiplier: 1, constant: 0)
let topConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0)
header.addConstraints([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])
self.table.tableHeaderView = header
self.header = header
self.postBody = postBody
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let text = postBody!.attributedText!
let height = text.boundingRectWithSize(
CGSizeMake(table.bounds.size.width, CGFloat.max),
options: [.UsesLineFragmentOrigin],
context: nil
).height
header!.frame.size.height = height
}
You might also want to use the code in stefandouganhyde's answer. It does not really matter how you calculate the height. The point is that autolayout won't work automatically for tableHeaderView.
Result:
We use NSLayoutManager to quickly estimate the height for items that need to resize based on the text. This is the basic idea:
override class func estimatedHeightForItem(text: String, atWidth width: CGFloat) -> CGFloat {
let storage = NSTextStorage(string: text!)
let container = NSTextContainer(size: CGSize(width: width, height: CGFloat.max))
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(container)
storage.addLayoutManager(layoutManager)
storage.addAttribute(NSFontAttributeName, value: UIFont.Body, range: NSRange(location: 0, length: storage.length))
container.lineFragmentPadding = 0.0
return layoutManager.usedRectForTextContainer(container).size.height
}
Beslan's answer is probably a better fit for your use case, but I find it nice to have more control how the layout is handled.
//may be it will help for you.
header = UIView(frame: CGRectMake(tableview.frame.origin.x,tableview.frame.origin.y, tableview.frame.size.width, 40))
header.backgroundColor = UIColor.yellowColor()
//Create Label and add it to the header
postBody = UILabel(frame: header.frame)
postBody.text = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog."
postBody.font = UIFont(name: "Lato-Regular", size: 16.0)
postBody.numberOfLines = 0
postBody.backgroundColor = FlatLime()
header.addSubview(postBody)
let maximumLabelSize: CGSize = CGSizeMake(postBody.size.width, CGFloat.max);
let options: NSStringDrawingOptions = NSStringDrawingOptions.UsesLineFragmentOrigin
let context: NSStringDrawingContext = NSStringDrawingContext()
context.minimumScaleFactor = 0.8
let attr: Dictionary = [NSFontAttributeName: postBody.font!]
var size: CGSize? = postBody.text?.boundingRectWithSize(maximumLabelSize, options:options, attributes: attr, context: context).size
let frame = header.frame
frame.size.height = size?.height
header.frame = frame
postBody.frame = frame
tableView!.tableHeaderView = header
you can calculate the height of a label by using its string
let labelWidth = label.frame.width
let maxLabelSize = CGSize(width: labelWidth, height: CGFloat.max)
let actualLabelSize = label.text!.boundingRectWithSize(maxLabelSize, options: [.UsesLineFragmentOrigin], attributes: [NSFontAttributeName: label.font], context: nil)
let labelHeight = actualLabelSize.height

UITableView damping animation and layout constraints

I'm trying to animate UITableView to act like a dropdownMenu by using its height constraint and UIView.animateWithDamping(..) block. I'm occuring weird problem with white background under tableView.
iPhone Simulator showing the problem
I have cleared each background color and it doesn't help much.
Here is the code setting all subviews of dropDownView, which is a UIView:
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.elements = []
defaultSetup()
}
private func defaultSetup() {
configureActionButton()
configureTableView()
}
private func configureActionButton() {
actionButton = UIButton(frame: CGRectZero)
actionButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(actionButton)
guard let superview = actionButton.superview else {
assert(false, "ActionButton adding to superview failed.")
return
}
// Constraints
actionButton.constrain(.Leading, .Equal, superview, .Leading, constant: 0, multiplier: 1)?.constrain(.Trailing, .Equal, superview, .Trailing, constant: 0, multiplier: 1)?.constrain(.Top, .Equal, superview, .Top, constant: 0, multiplier: 1)?.constrain(.Bottom, .Equal, superview, .Bottom, constant: 0, multiplier: 1)
// Appearance
actionButton.backgroundColor = UIColor.clearColor()
actionButton.opaque = false
actionButton.contentHorizontalAlignment = .Left
actionButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 0)
if borderVisible {
actionButton.layer.cornerRadius = 5
actionButton.layer.borderColor = UIColor.blackColor().CGColor
actionButton.layer.borderWidth = 1
actionButton.clipsToBounds = true
}
// Actions
actionButton.addTarget(self, action: "menuAction:", forControlEvents: .TouchUpInside)
}
private func configureTableView() {
tableView = BOTableView(frame: CGRectZero, items: elements, configuration: configuration)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.dataSource = self
addSubview(tableView)
guard let tableViewSuperview = tableView.superview else {
assert(false, "TableView adding to superview failed.")
return
}
// Constraints
tableView.constrain(.Trailing, .Equal, tableViewSuperview, .Trailing, constant: 0, multiplier: 1)?.constrain(.Top, .Equal, tableViewSuperview, .Bottom, constant: 0, multiplier: 1)?.constrain(.Leading, .Equal, tableViewSuperview, .Leading, constant: 0, multiplier: 1)
tvHeightConstraint = NSLayoutConstraint(item: tableView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 0)
tableView.addConstraint(tvHeightConstraint)
}
BOTableView class initializer:
init(frame: CGRect, items: [String], configuration: BOConfiguration) {
super.init(frame: frame, style: UITableViewStyle.Plain)
self.items = items
self.selectedIndexPath = NSIndexPath(forRow: 0, inSection: 0)
self.configuration = configuration
// Setup table view
self.opaque = false
self.backgroundView?.backgroundColor = UIColor.clearColor()
self.backgroundColor = UIColor.clearColor()
self.separatorColor = UIColor.blackColor()
self.scrollEnabled = false
self.separatorStyle = .SingleLine
self.layer.cornerRadius = 5
self.layer.borderColor = UIColor.blackColor().CGColor
self.layer.borderWidth = 1
self.clipsToBounds = true
}
UIView animations:
private func showMenuWithCompletionBlock(completion: (succeeded: Bool) -> Void) {
delegate?.menuWillShow(self)
let tvHeight = frame.size.height * CGFloat(elements.count)
tvHeightConstraint.constant = tvHeight
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.5, options: .CurveEaseInOut, animations: { [weak self] () -> Void in
guard let strongSelf = self else {
completion(succeeded: false)
return
}
strongSelf.layoutIfNeeded()
}, completion: { (finished) -> Void in
if finished {
completion(succeeded: true)
}
})
}
Here is the code for UIView + Constraints extension, used in code:
extension UIView {
/**
:returns: true if v is in this view's super view chain
*/
public func isSuper(v : UIView) -> Bool
{
for var s : UIView? = self; s != nil; s = s?.superview {
if(v == s) {
return true;
}
}
return false
}
public func constrain(attribute: NSLayoutAttribute, _ relation: NSLayoutRelation, _ otherView: UIView, _ otherAttribute: NSLayoutAttribute, constant: CGFloat = 0.0, multiplier : CGFloat = 1.0) -> UIView?
{
let c = NSLayoutConstraint(item: self, attribute: attribute, relatedBy: relation, toItem: otherView, attribute: otherAttribute, multiplier: multiplier, constant: constant)
if isSuper(otherView) {
otherView.addConstraint(c)
return self
}
else if(otherView.isSuper(self) || otherView == self)
{
self.addConstraint(c)
return self
}
assert(false)
return nil
}
public func constrain(attribute: NSLayoutAttribute, _ relation: NSLayoutRelation, constant: CGFloat, multiplier : CGFloat = 1.0) -> UIView?
{
let c = NSLayoutConstraint(item: self, attribute: attribute, relatedBy: relation, toItem: nil, attribute: .NotAnAttribute, multiplier: multiplier, constant: constant)
self.addConstraint(c)
return self
}
}
When I tried to debug the views' hierarchy in debugger, the only view which had white background was tableView, but I have cleared the background in code. I have also tried to set tableView's backgroundView to nil as well as backgroundView.backgroundColor to clearColor(). Nothing changed.
Maybe try to set the UITableView footer to a blank view, don't really know why, but it seams to help for similar issue like You have.
[_tableView setTableFooterView:[[UIView alloc] init]];

Swift : How to make a Black screen to guidelines for user to use app

Im using swift 1.2.
I need to create a guideline for user to use my app.
it look like: a black transparent screen, 1 button highlight with note how to use, what is it.
i have searching for solution but still get stuck.
Can anybody help me?
Thanks!
If I understood correctly what you want, it can be done something like this*:
class TutorialView : UIView
{
override init(frame: CGRect)
{
super.init(frame: frame)
self.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.5)
}
convenience init()
{
self.init(frame: CGRectZero)
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
func showFromWindow()
{
let window = UIApplication.sharedApplication().keyWindow!
self.translatesAutoresizingMaskIntoConstraints = false
window.addSubview(self)
window.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[tutorialView]-0-|", options: NSLayoutFormatOptions.DirectionLeftToRight, metrics: nil, views: ["tutorialView": self]))
window.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[tutorialView]-0-|", options: NSLayoutFormatOptions.DirectionLeftToRight, metrics: nil, views: ["tutorialView": self]))
}
func addCommentToTopRightItem(comment: String, afterDelay: NSTimeInterval)
{
let topMargin : Int32 = 54
let rightMargin : Int32 = 24
let height : Int32 = 100
// we need two views - one for arrow and second for text
let commentLabel = UILabel()
let arrowView = UIView()
commentLabel.translatesAutoresizingMaskIntoConstraints = false
arrowView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(commentLabel)
self.addSubview(arrowView)
// setup layout for views
// |^^^^^|
// |^^^^^|arrow|
// |label|_____|
// |_____|
//
// we place center of Y label to the bottom of arrow view
// to an arrow coming out of the middle of text
let metrics = ["topmargin" : NSNumber(int: topMargin), "rightmargin": NSNumber(int: rightMargin),"height" : NSNumber(int: height)]
let views = ["label": commentLabel, "arrow": arrowView]
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-topmargin-[arrow(==height)]", options: NSLayoutFormatOptions.DirectionLeftToRight, metrics: metrics, views: views))
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[label(==height)]", options: NSLayoutFormatOptions.DirectionLeftToRight, metrics: metrics, views: views))
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-rightmargin-[arrow(==height)]-[label]-0-|", options: NSLayoutFormatOptions.DirectionRightToLeft, metrics: metrics, views: views))
self.addConstraint(NSLayoutConstraint(item: commentLabel, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: arrowView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 1.0))
// configure text label
commentLabel.textColor = UIColor.whiteColor()
commentLabel.numberOfLines = 0
commentLabel.textAlignment = NSTextAlignment.Right
commentLabel.font = UIFont(name: "HelveticaNeue-Light", size: 16)
commentLabel.text = comment
// configure arrow view
self.layoutIfNeeded()
let d : CGFloat = min(arrowView.frame.size.height, arrowView.frame.size.width)
// draw arc line from {0, 1} to {1, 0}
// | /|
// |/_|
let linePath = UIBezierPath()
linePath.moveToPoint(CGPointMake(0, d))
linePath.addCurveToPoint(CGPointMake(d, 0), controlPoint1: CGPointMake(d*0.5, d), controlPoint2: CGPointMake(d, d*0.5))
let lineLayer = CAShapeLayer()
lineLayer.path = linePath.CGPath
lineLayer.strokeColor = UIColor.redColor().CGColor
lineLayer.backgroundColor = UIColor.clearColor().CGColor
lineLayer.fillColor = UIColor.clearColor().CGColor
lineLayer.lineWidth = 2
// draw triangle near {1, 0} point
let trianglePath = UIBezierPath()
trianglePath.moveToPoint(CGPointMake(d, 0))
trianglePath.addLineToPoint(CGPointMake(d - 5, 15))
trianglePath.addLineToPoint(CGPointMake(d + 5, 15))
trianglePath.closePath()
let triangleLayer = CAShapeLayer()
triangleLayer.path = trianglePath.CGPath
triangleLayer.strokeColor = UIColor.redColor().CGColor
triangleLayer.backgroundColor = UIColor.clearColor().CGColor
triangleLayer.fillColor = UIColor.redColor().CGColor
triangleLayer.lineWidth = 2
// line + triangle = arrow :)
arrowView.layer.addSublayer(lineLayer)
arrowView.layer.addSublayer(triangleLayer)
arrowView.alpha = 0.0
commentLabel.alpha = 0.0
UIView.animateWithDuration(0.8, delay: afterDelay, options: UIViewAnimationOptions.AllowAnimatedContent, animations:
{ () -> Void in
arrowView.alpha = 1.0
commentLabel.alpha = 1.0
}, completion: nil)
}
func addButton(title: String, highlighteAfterDelay: NSTimeInterval)
{
// same as before
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(button)
self.addConstraint(NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 1.0))
self.addConstraint(NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: -20.0))
self.addConstraint(NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 120.0))
self.addConstraint(NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 40.0))
button.setTitle(title, forState: UIControlState.Normal)
button.setTitleColor(UIColor.whiteColor(), forState: UIControlState.Normal)
button.layer.cornerRadius = 20.0
button.layer.masksToBounds = false
button.layer.borderWidth = 1.0
button.layer.borderColor = UIColor.whiteColor().CGColor
button.layer.shadowColor = UIColor.whiteColor().CGColor
button.layer.shadowOpacity = 0.4
button.layer.shadowRadius = 4
button.layer.shadowOffset = CGSizeMake(0, 0)
button.alpha = 0.2
button.enabled = false
UIView.animateWithDuration(0.8, delay: highlighteAfterDelay, options: UIViewAnimationOptions.AllowAnimatedContent, animations:
{ () -> Void in
button.alpha = 1.0
button.enabled = true
}, completion: nil)
}
}
In some view controller:
let tutorial = TutorialView()
tutorial.showFromWindow()
tutorial.addCommentToTopRightItem("Tap this button to do something amazing action!", afterDelay: 1.0)
tutorial.addButton("Done", highlighteAfterDelay: 3.0)
Result:
*I don't have a compiler version 1.2 but I think that there should be no serious differences.
I see the problem now, you can add UView on top of it, give it a color with alpha, and get frame by using
convertRect(button.frame, toView:overlayView)
Hopefuly helps

Resources