Programatically laying out subviews in a stackView - ios

I would like a UITableCell to contain a variable number of buttons, for example "Yes" and "No" buttons, or "Red", "Green" and "Blue" buttons. I've tried various approached but settled on using a vertical stackView within the cell and programmatically adding a view containing the button (feel free to suggest a better approach here).
The issue I'm having is that I can't figure out what auto layout is doing. Normally, when you add views to a stackView in interface builder, you just need a height value and then they stack nicely. However the following code appears to give them a width of zero and if I set the width, the views are all on top of each other. Note this doesn't include the buttons I've mentioned above to make responding easier; I've just added two views.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "formQuestion") as! FormQuestionTableViewCell
let view = UIView(frame: .zero)
view.backgroundColor = UIColor.red
view.translatesAutoresizingMaskIntoConstraints = false
cell.questionOptions.addSubview(view)
NSLayoutConstraint.activate([
view.heightAnchor.constraint(equalToConstant: 8)
// , view.widthAnchor.constraint(equalToConstant: 16)
])
let view2 = UIView(frame: .zero)
view2.backgroundColor = UIColor.blue
view2.translatesAutoresizingMaskIntoConstraints = false
cell.questionOptions.addSubview(view2)
NSLayoutConstraint.activate([
view2.heightAnchor.constraint(equalToConstant: 32)
// , view2.widthAnchor.constraint(equalToConstant: 64)
])
return cell
}
And for the sake of completeness, the FormQuestionTableViewCell looks like this...
class FormQuestionTableViewCell: UITableViewCell {
#IBOutlet weak var descriptionContainer: UIView!
#IBOutlet weak var controlContainer: UIView!
#IBOutlet weak var descriptionLabel: UILabel!
#IBOutlet weak var questionOptions: UIStackView!
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
}
This looks like this when the width constraints are enabled (though I believe this shouldn't be necessary)...
Any help you can give me would be very much appreciated!

This is wrong:
cell.questionOptions.addSubview(view)
When adding a subview to UIStackView, you should use:
.addArrangedSubview()
Reference:
https://developer.apple.com/documentation/uikit/uistackview/1616227-addarrangedsubview

You are saying
cell.questionOptions.addSubview(view)
That's wrong. questionOptions is a stack view. If you want it to take responsibility for laying out view, you must say
cell.questionOptions.addArrangedSubview(view)

Related

How can I dynamically set the height of custom UIView class with XIB added to a vertical stackview?

Background info
I am creating a consent/question form application. This form will have several different questions with text in different lengths. One line up to 5 lines depending on the device.
This is what i have done so far
I have created a custom UIView class with an XIB file where i have added one Label anchored to the top (Lines set to 0 = multiline for that label). This label will a short or a long text added to it.
Underneath the label i have a yes/no buttons as radio group lookalike and under there again I will have a UIText field hidden by default, that will only be visible on some of the question and only if the user selects "Yes"
The question will be added to a vertical UIStackview who sits on top of a UIScrollView
myQuestionView = MyQuestionView(preDefinedAnswer: false, preDefinedAnswerText: nil)
myQuestionView?.questionLabel.text = "Lorem ipsum dolor sit amet, consectetur adipiscing ?"
myQuestionView?.shouldShowAnswerTextfield = true
myQuestionView?.heightAnchor.constraint(equalToConstant: 120).isActive = true
myStackView.addArrangedSubview(myQuestionView!)
The problem
As you can see in the code above, I am setting the height constraint to 120, and that looks ok if the question is only one line and if the user DONT select yes (then the view should grow and show the answertextfield). Normally i would maybe created a height constraint outlet and tried to calculate the height within the UIView class, but I am not able to to that.
The question
How can i dynamically set the height of my custom UIView class based on its content AND get the stackview to grow accordingly? I want the height to be correct when i instantiate it and the grow if the user clicks yes (to show the UITextfield).
Thanks for any help.
The UIView class
import UIKit
class MyQuestionView: UIView {
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var answerTextfield: UITextField!
#IBOutlet weak var jaButton: UIButton!
#IBOutlet weak var neiButton: UIButton!
#IBInspectable var height: CGFloat = 300.0
#IBOutlet weak var viewHeight: NSLayoutConstraint!
var shouldShowAnswerTextfield : Bool?
var answerText : String?
var didAnswerYes : Bool?
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
init(preDefinedAnswer:Bool?, preDefinedAnswerText:String?)
{
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 200) )
didAnswerYes = preDefinedAnswer
answerText = preDefinedAnswerText
commonInit()
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
setup()
print("init")
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
setup()
print("Init?")
}
func commonInit(){
let viewFromXib = Bundle.main.loadNibNamed("MyQuestionView", owner: self, options: nil)![0] as! UIView
viewFromXib.frame = self.bounds
addSubview(viewFromXib)
}
func setup()
{
answerTextfield.isHidden = true
if(didAnswerYes != nil)
{
print("Setup \(didAnswerYes)")
setAnswer(didAnswerYes!)
if(answerText != nil)
{
answerTextfield.text = answerText
if(didAnswerYes!)
{
answerTextfield.isHidden = false
}
}
}
//sett constraints
//self.heightAnchor.constraint(equalToConstant: 320).isActive = true
//self.sizeToFit()
//self.layoutIfNeeded()
}
Possible ways:
Use table view with the dynamic cell for each question view/cell so, no need to manage more things.
Find the label/question/string height (https://stackoverflow.com/a/30450559/14733292) and calculate the final height like label height + text field height (if the text field is hidden then pass zero) + other space and component height and assign this height to XIB height.
Don't give height constraint. Give top, bottom, leading, trailing constraint to all XIB subview properly.
For more check this: https://stackoverflow.com/a/47216020/14733292

Active constraints in UITableViewCell don't get updated when updated from cellForRowAt

I have a view with two leading constraints that conflict each other. One with leading offset, other without. The 'constant' property works for active constraint. But I want to enable/disable constraints to fit my needs. It works in every place except UITableViewCell, when called from tableView:cellForRowAtIndexPath:.
Here is my cell:
class WowCell: UITableViewCell {
#IBOutlet weak var myView: UIView!
#IBOutlet var leading: NSLayoutConstraint!
#IBOutlet var leadingSpace: NSLayoutConstraint!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setup(ye:Bool) {
setActiveConstraint(ye: ye)
//doesn't help
setNeedsLayout()
layoutIfNeeded()
needsUpdateConstraints()
setNeedsUpdateConstraints()
needsUpdateConstraints()
setNeedsLayout()
setNeedsDisplay()
}
func setActiveConstraint(ye:Bool){
leading.isActive = !ye
leadingSpace.isActive = ye
}
override func layoutSubviews() {
//when called from here, it does work
}
}
Thanks in advance
Edit: As Prashant Tukadiya advised, it works by changing priorities of both active constraints. But be aware that both constraint have to be lower than 1000.
func setActiveConstraint(ye:Bool){
leading.priority = UILayoutPriority(rawValue: !ye ? 999 : 250)
leadingSpace.priority = UILayoutPriority(rawValue: ye ? 999 : 250)
}
When you deactive your constraint it will be release by ARC because it's weak property in your vc.
You can change by removing weak to handle this.
#IBOutlet var leading: NSLayoutConstraint!
#IBOutlet var leadingSpace: NSLayoutConstraint!
But you need the release them right after they aren't needed if you don't want a memory leak
Try with priority as active / deactivate constraint call addConstraint and removeConstraint internally
func setActiveConstraint(ye:Bool){
leading.priority = !ye ? 999 : 250
leading.priority = ye ? 999 : 250
}
There's a way to do that without changing priorities:
Override updateConstraints() in your UITableViewCell
Activate/deactivate constraint there
Make sure to call super.updateConstraints() at the end

self sizing UITableViewCell goes small

I'm trying to create a table view cell prototype (similar to one below) programmatically.
The designed the cell with two stack views,
a) a vertical stack view to contain the text labels and,
b) a horizontal stack view to contain the image view & vertical stack view
I create the required views, stuff it in stack view, and pin stack view to table cell's contentView in the init() of tableviewcell.
And from cellForItemAtIndexPath I call configureCell() to populate data of the cell.
My init() looks like this
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
textStackView = UIStackView(arrangedSubviews: [priorityNameLabel, descriptionLabel])
textStackView.axis = .vertical
textStackView.alignment = .leading
textStackView.distribution = .fill
textStackView.spacing = 5
containerStackView = UIStackView(arrangedSubviews: [priorityImageView, textStackView])
containerStackView.axis = .horizontal
containerStackView.alignment = .center
containerStackView.spacing = 5
containerStackView.distribution = .fill
contentView.addSubview(containerStackView)
containerStackView.translatesAutoresizingMaskIntoConstraints = false
pinContainerToSuperview()
}
func pinContainerToSuperview() {
containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).activate()
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor).activate()
containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).activate()
containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).activate()
}
In my view controller, I set tableView rowHeight to automaticDimension and estimated height to some approx. value. When I run the code, all I'm getting is,
The narrow horizontal lines on the top of the image are my tableview cells (My data count is 3 in this case). I couldn't figure out the problem. Can someone point out what's going wrong here?
EDIT 1:
These are the instance members of my TableViewCell class
var containerStackView: UIStackView!
var textStackView: UIStackView!
var priorityImageView: UIImageView! {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}
var priorityNameLabel: UILabel! {
let label = UILabel()
return label
}
var descriptionLabel: UILabel! {
let label = UILabel()
return label
}
You have your labels and image view as computed properties - this means that every time you access them a new instance is created. This is bad. This basically means that when you set them as arranged subvies of your stack views and then try to configure them later, you work on different objects.
Simplest solution would be to create your labels and image view the way you create your stack views in init.

How do you programmatically make UIViews which are the height of textLabels inside them? (In Swift)

I'm trying to make UIViews which each contain different statements of text (In UITextViews). There can be a varying number of views and each statement can be different in length. I make these views using
let newView = DragView(heightOfView: ???, viewNumber: i, heightFromTop: currentHeightForThings)
In the DragView class I then access the statement using the viewNumber and put the statement in the text label in a nib file I've made.
My issue is I have nothing to put in heightOfView. The height I want is the height of the textLabel which varies depending on how many lines are in the textView for the statement. However I can't access this height because the textLabel isn't built yet.
Thanks in advance, I'm new to swift but want to learn fast so I apologise if I'm missing something obvious!
Heres the code I have in the class DragView
class DragView: UIView {
#IBOutlet var dragView: UIView!
#IBOutlet weak var statementLabel: UITextView!
var dropTarget: UIView?
var viewNumber: Int!
init(heightOfView: Int, viewNumber:Int, heightFromTop: Int) {
self.viewNumber = viewNumber
let startingPosition = CGRect(x: Int(widthCentre) - dragViewWidth / 2, y: heightFromTop, width: dragViewWidth, height: heightOfView)
super.init(frame: startingPosition)
NSBundle.mainBundle().loadNibNamed("DragView", owner: self, options: nil)
self.addSubview(self.dragView)
let movingView = MovingView(frame: startingPosition)
self.addSubview(movingView)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Here the movingView is a subview I add over the view to move the view.
You can override the DragView's sizeThatFits method to have it return a height based on the statementLabel's height. You will have to first call sizeToFit on the textView which will set the height for that view, then return a height based on that.
override func sizeThatFits(size: CGSize) -> CGSize {
self.statementLabel.sizeToFit()
return CGSize(
width: self.frame.width,
height: self.statementLabel.frame.height)
}
Additionally, I would recommend looking into sizeThatFits and layoutSubviews if you are going to be doing programatic layout. The sizing and positioning of subviews should be taking place in layoutSubviews rather than init.

Can't change UITableViewCell's subview's position

So I have a static tableview in place and I'd like to change the position of the label inside the first tableviewcell. I have IBOutlet for the first cell.
This is how I try to change the label's position:
UILabel *label1 = [tempTC.cell1.contentView.subviews firstObject];
label1.frame = CGRectMake(10, 10, 10, 10);
The problem is that the frame doesn't change, however I can do everything else to the label like change it's text and size etc, but changing the frame doesn't seem to work.
Is there something I am missing?
When using auto layout, you should set frames, you should change the constraints instead. The easiest way to do this is to make IBOutlets to the ones you need to change, and change the constant value of the constraint. For instance, if you had a constraint to the left side called leftCon, you could do something like this:
self.leftCon.constant = 30;
When you want to change frame subview those were constraint in XIB. You need to set translatesAutoresizingMaskIntoConstraints = YES.
label1.translatesAutoresizingMaskIntoConstraints = YES;
label1.frame = CGRectMake(10, 10, 10, 10);
For detail:
Can I use setFrame and autolayout on the same view?
First of all according to comments, disable auto layout.
Second if You want refer to textLabel use [[UITableViewCell textLabel] setFrame:CGRectMake(10,10,10,10)];
BTW. If You'r first UITablViewCell is special I suggest use .xib and subclass UITableviewCell. Apply that subclass to prototype UITableViewCell in storyboard, then add this cell protype cell as property/global variable.
If you need more description please ask :).
Make a custom UITableViewCell.
Look at the example below.
Inside tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) method I am dynamically changing TimeStatus (UIView) frame size:
.
.
.
let cell:OneTVChannelTableViewCell = tableView.dequeueReusableCellWithIdentifier(OneCurrentChannelTCIdentifier, forIndexPath: indexPath) as OneTVChannelTableViewCell
.
.
.
cell.setForRepairDynamicViewWidth(channelTimeStatusWidth)
.
.
.
And here is my simple custom UITableViewCell:
class OneTVChannelTableViewCell: UITableViewCell {
#IBOutlet weak var Number: UILabel!
#IBOutlet weak var Image: UIImageView!
#IBOutlet weak var Title: UILabel!
#IBOutlet weak var TimeStatus: UIView!
#IBOutlet weak var TimeBack: UIView!
var RepairWidth:CGFloat = 0.0
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
override func layoutSubviews() {
super.layoutSubviews()
TimeStatus.frame.size.width = RepairWidth
}
func setForRepairDynamicViewWidth(width:CGFloat)
{
RepairWidth = width
}
}

Resources