Configure automatically components in UIStackView (Programatically) - ios

Hi. As you can see, components are overlapped in a UIStackView and I am struggling with that.
Here are some code snippets
class BottomSheetController: UIViewController {
lazy var headerLabel: UILabel = {
let label = UILabel()
label.text = "Workout"
return label
}()
lazy var header: UIView = {
let uiView = UIView()
return uiView
}()
lazy var workoutInput: UITextField = {
let textField = UITextField()
textField.placeholder = "Find or add option"
textField.sizeToFit()
textField.clipsToBounds = true
textField.backgroundColor = UIColor(red: 65 / 255.0, green: 65 / 255.0, blue: 65 / 255.0, alpha: 0.4)
textField.layer.cornerRadius = 5
textField.setImage(image: UIImage(named: "search_black")!)
textField.setClearTextButton()
return textField
}()
lazy var tagsView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.backgroundColor = .red
return scrollView
}()
lazy var containerStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [header, workoutInput, tagsView])
stackView.clipsToBounds = true
stackView.axis = .vertical
stackView.backgroundColor = .systemGray2
return stackView
}()
// ...
private func setConstraint() {
header.addSubview(headerLabel)
header.addSubview(doneButton)
containerStackView.snp.makeConstraints { make in
make.top.left.right.bottom.equalToSuperview().inset(20)
}
headerLabel.snp.makeConstraints { make in
make.top.centerX.equalToSuperview()
}
doneButton.snp.makeConstraints { make in
make.top.right.equalToSuperview()
}
workoutInput.snp.makeConstraints {make in
make.height.equalTo(40)
}
}
}
My problem might be, header view doesn't have constant height value
But I don't want to set the magic number to header view. But rather assign the value corresponding to child views (workoutInput)
Is there fancy ways to adjust the height?
How to prevent components from overlapping?

You need to add enough constraints to satisfy auto-layout requirements.
A UIView has no intrinsic size... so your header ends up with a height of Zero.
Make a couple changes to control the height of that view:
private func setConstraint() {
header.addSubview(headerLabel)
header.addSubview(doneButton)
containerStackView.snp.makeConstraints { make in
// constrain to safe area, not to superView
make.top.left.right.bottom.equalTo(view.safeAreaLayoutGuide).inset(20)
}
headerLabel.snp.makeConstraints { make in
make.top.centerX.equalToSuperview()
// constrain label bottom to superview
make.bottom.equalToSuperview()
}
doneButton.snp.makeConstraints { make in
make.top.right.equalToSuperview()
// constrain button bottom to superview
make.bottom.equalToSuperview()
}
workoutInput.snp.makeConstraints {make in
make.height.equalTo(40)
}
}
As a side note, you'll find it much easier to debug your layout if you give your UI elements contrasting background colors, such as:
header.backgroundColor = .systemTeal
headerLabel.backgroundColor = .yellow
doneButton.backgroundColor = .green
With those changes, this is what you should see:

I believe you should be able to do header.frame.size.height = [height] in the viewDidLoad() function and it should change it like that.

Related

UILabel not clickable in stack view programmatically created Swift

My question and code is based on this answer to one of my previous questions. I have programmatically created stackview where several labels are stored and I'm trying to make these labels clickable. I tried two different solutions:
Make clickable label. I created function and assigned it to the label in the gesture recognizer:
public func setTapListener(_ label: UILabel){
let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureMethod(_:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
label.isUserInteractionEnabled = true
label.addGestureRecognizer(tapGesture)
}
#objc func tapGestureMethod(_ gesture: UITapGestureRecognizer) {
print(gesture.view?.tag)
}
but it does not work. Then below the second way....
I thought that maybe the 1st way does not work because the labels are in UIStackView so I decided to assign click listener to the stack view and then determine on which view we clicked. At first I assigned to each of labels in the stackview tag and listened to clicks:
let tap = UITapGestureRecognizer(target: self, action: #selector(didTapCard(sender:)))
labelsStack.addGestureRecognizer(tap)
....
#objc func didTapCard (sender: UITapGestureRecognizer) {
(sender.view as? UIStackView)?.arrangedSubviews.forEach({ label in
print((label as! UILabel).text)
})
}
but the problem is that the click listener works only on the part of the stack view and when I tried to determine on which view we clicked it was not possible.
I think that possibly the problem is with that I tried to assign one click listener to several views, but not sure that works as I thought. I'm trying to make each label in the stackview clickable, but after click I will only need getting text from the label, so that is why I used one click listener for all views.
Applying a transform to a view (button, label, view, etc) changes the visual appearance, not the structure.
Because you're working with rotated views, you need to implement hit-testing.
Quick example:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// convert the point to the labels stack view coordinate space
let pt = labelsStack.convert(point, from: self)
// loop through arranged subviews
for i in 0..<labelsStack.arrangedSubviews.count {
let v = labelsStack.arrangedSubviews[i]
// if converted point is inside subview
if v.frame.contains(pt) {
return v
}
}
return super.hitTest(point, with: event)
}
Assuming you're still working with the MyCustomView class and layout from your previous questions, we'll build on that with a few changes for layout, and to allow tapping the labels.
Complete example:
class Step5VC: UIViewController {
// create the custom "left-side" view
let myView = MyCustomView()
// create the "main" stack view
let mainStackView = UIStackView()
// create the "bottom labels" stack view
let bottomLabelsStack = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
guard let img = UIImage(named: "pro1") else {
fatalError("Need an image!")
}
// create the image view
let imgView = UIImageView()
imgView.contentMode = .scaleToFill
imgView.image = img
mainStackView.axis = .horizontal
bottomLabelsStack.axis = .horizontal
bottomLabelsStack.distribution = .fillEqually
// add views to the main stack view
mainStackView.addArrangedSubview(myView)
mainStackView.addArrangedSubview(imgView)
// add main stack view and bottom labels stack view to view
mainStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mainStackView)
bottomLabelsStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bottomLabelsStack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain Top/Leading/Trailing
mainStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
mainStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
//mainStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// we want the image view to be 270 x 270
imgView.widthAnchor.constraint(equalToConstant: 270.0),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
// constrain the bottom lables to the bottom of the main stack view
// same width as the image view
// aligned trailing
bottomLabelsStack.topAnchor.constraint(equalTo: mainStackView.bottomAnchor),
bottomLabelsStack.trailingAnchor.constraint(equalTo: mainStackView.trailingAnchor),
bottomLabelsStack.widthAnchor.constraint(equalTo: imgView.widthAnchor),
])
// setup the left-side custom view
myView.titleText = "Gefährdung"
let titles: [String] = [
"keine / gering", "mittlere", "erhöhte", "hohe",
]
let colors: [UIColor] = [
UIColor(red: 0.863, green: 0.894, blue: 0.527, alpha: 1.0),
UIColor(red: 0.942, green: 0.956, blue: 0.767, alpha: 1.0),
UIColor(red: 0.728, green: 0.828, blue: 0.838, alpha: 1.0),
UIColor(red: 0.499, green: 0.706, blue: 0.739, alpha: 1.0),
]
for (c, t) in zip(colors, titles) {
// because we'll be using hitTest in our Custom View
// we don't need to set .isUserInteractionEnabled = true
// create a "color label"
let cl = colorLabel(withColor: c, title: t, titleColor: .black)
// we're limiting the height to 270, so
// let's use a smaller font for the left-side labels
cl.font = .systemFont(ofSize: 12.0, weight: .light)
// create a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTapRotatedLeftLabel(_:)))
// add the recognizer to the label
cl.addGestureRecognizer(t)
// add the label to the custom myView
myView.addLabel(cl)
}
// rotate the left-side custom view 90-degrees counter-clockwise
myView.rotateTo(-.pi * 0.5)
// setup the bottom labels
let colorDictionary = [
"Red":UIColor.systemRed,
"Green":UIColor.systemGreen,
"Blue":UIColor.systemBlue,
]
for (myKey,myValue) in colorDictionary {
// bottom labels are not rotated, so we can add tap gesture recognizer directly
// create a "color label"
let cl = colorLabel(withColor: myValue, title: myKey, titleColor: .white)
// let's use a smaller, bold font for the left-side labels
cl.font = .systemFont(ofSize: 12.0, weight: .bold)
// by default, .isUserInteractionEnabled is False for UILabel
// so we must set .isUserInteractionEnabled = true
cl.isUserInteractionEnabled = true
// create a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTapBottomLabel(_:)))
// add the recognizer to the label
cl.addGestureRecognizer(t)
bottomLabelsStack.addArrangedSubview(cl)
}
}
#objc func didTapRotatedLeftLabel (_ sender: UITapGestureRecognizer) {
if let v = sender.view as? UILabel {
let title = v.text ?? "label with no text"
print("Tapped Label in Rotated Custom View:", title)
// do something based on the tapped label/view
}
}
#objc func didTapBottomLabel (_ sender: UITapGestureRecognizer) {
if let v = sender.view as? UILabel {
let title = v.text ?? "label with no text"
print("Tapped Bottom Label:", title)
// do something based on the tapped label/view
}
}
func colorLabel(withColor color:UIColor, title:String, titleColor:UIColor) -> UILabel {
let newLabel = PaddedLabel()
newLabel.padding = UIEdgeInsets(top: 6, left: 8, bottom: 6, right: 8)
newLabel.backgroundColor = color
newLabel.text = title
newLabel.textAlignment = .center
newLabel.textColor = titleColor
newLabel.setContentHuggingPriority(.required, for: .vertical)
return newLabel
}
}
class MyCustomView: UIView {
public var titleText: String = "" {
didSet { titleLabel.text = titleText }
}
public func addLabel(_ v: UIView) {
labelsStack.addArrangedSubview(v)
}
public func rotateTo(_ d: Double) {
// get the container view (in this case, it's the outer stack view)
if let v = subviews.first {
// set the rotation transform
if d == 0 {
self.transform = .identity
} else {
self.transform = CGAffineTransform(rotationAngle: d)
}
// remove the container view
v.removeFromSuperview()
// tell it to layout itself
v.setNeedsLayout()
v.layoutIfNeeded()
// get the frame of the container view
// apply the same transform as self
let r = v.frame.applying(self.transform)
wC.isActive = false
hC.isActive = false
// add it back
addSubview(v)
// set self's width and height anchors
// to the width and height of the container
wC = self.widthAnchor.constraint(equalToConstant: r.width)
hC = self.heightAnchor.constraint(equalToConstant: r.height)
guard let sv = v.superview else {
fatalError("no superview")
}
// apply the new constraints
NSLayoutConstraint.activate([
v.centerXAnchor.constraint(equalTo: self.centerXAnchor),
v.centerYAnchor.constraint(equalTo: self.centerYAnchor),
wC,
outerStack.widthAnchor.constraint(equalTo: sv.heightAnchor),
])
}
}
// our subviews
private let outerStack = UIStackView()
private let titleLabel = UILabel()
private let labelsStack = UIStackView()
private var wC: NSLayoutConstraint!
private var hC: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// stack views and label properties
outerStack.axis = .vertical
outerStack.distribution = .fillEqually
labelsStack.axis = .horizontal
// let's use .fillProportionally to help fit the labels
labelsStack.distribution = .fillProportionally
titleLabel.textAlignment = .center
titleLabel.backgroundColor = .lightGray
titleLabel.textColor = .white
// add title label and labels stack to outer stack
outerStack.addArrangedSubview(titleLabel)
outerStack.addArrangedSubview(labelsStack)
outerStack.translatesAutoresizingMaskIntoConstraints = false
addSubview(outerStack)
wC = self.widthAnchor.constraint(equalTo: outerStack.widthAnchor)
hC = self.heightAnchor.constraint(equalTo: outerStack.heightAnchor)
NSLayoutConstraint.activate([
outerStack.centerXAnchor.constraint(equalTo: self.centerXAnchor),
outerStack.centerYAnchor.constraint(equalTo: self.centerYAnchor),
wC, hC,
])
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// convert the point to the labels stack view coordinate space
let pt = labelsStack.convert(point, from: self)
// loop through arranged subviews
for i in 0..<labelsStack.arrangedSubviews.count {
let v = labelsStack.arrangedSubviews[i]
// if converted point is inside subview
if v.frame.contains(pt) {
return v
}
}
return super.hitTest(point, with: event)
}
}
class PaddedLabel: UILabel {
var padding: UIEdgeInsets = .zero
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: padding))
}
override var intrinsicContentSize : CGSize {
let sz = super.intrinsicContentSize
return CGSize(width: sz.width + padding.left + padding.right, height: sz.height + padding.top + padding.bottom)
}
}
The problem is with the the stackView's height. Once the label is rotated, the stackview's height is same as before and the tap gestures will only work within stackview's bounds.
I have checked it by changing the height of the stackview at the transform and observed tap gestures are working fine with the rotated label but with the part of it inside the stackview.
Now the problem is that you have to keep the bounds of the label inside the stackview either by changing it axis(again a new problem as need to handle the layout with it) or you have to handle it without the stackview.
You can check the observation by clicking the part of rotated label inside stackview and outside stackview.
Code to check it:
class ViewController: UIViewController {
var centerLabel = UILabel()
let mainStackView = UIStackView()
var stackViewHeightCons:NSLayoutConstraint?
var stackViewTopsCons:NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
mainStackView.axis = .horizontal
mainStackView.alignment = .top
mainStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mainStackView)
mainStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
mainStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
stackViewTopsCons = mainStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 300)
stackViewTopsCons?.isActive = true
stackViewHeightCons = mainStackView.heightAnchor.constraint(equalToConstant: 30)
stackViewHeightCons?.isActive = true
centerLabel.textAlignment = .center
centerLabel.text = "Let's rotate this label"
centerLabel.backgroundColor = .green
centerLabel.tag = 11
setTapListener(centerLabel)
mainStackView.addArrangedSubview(centerLabel)
// outline the stack view so we can see its frame
mainStackView.layer.borderColor = UIColor.red.cgColor
mainStackView.layer.borderWidth = 1
}
public func setTapListener(_ label: UILabel){
let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureMethod(_:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
label.isUserInteractionEnabled = true
label.addGestureRecognizer(tapGesture)
}
#objc func tapGestureMethod(_ gesture: UITapGestureRecognizer) {
print(gesture.view?.tag ?? 0)
var yCor:CGFloat = 300
if centerLabel.transform == .identity {
centerLabel.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
yCor = mainStackView.frame.origin.y - (centerLabel.frame.size.height/2)
} else {
centerLabel.transform = .identity
}
updateStackViewHeight(topCons: yCor)
}
private func updateStackViewHeight(topCons:CGFloat) {
stackViewTopsCons?.constant = topCons
stackViewHeightCons?.constant = centerLabel.frame.size.height
}
}
Sorry. My assumption was incorrect.
Why are you decided to use Label instead of UIButton (with transparence background color and border line)?
Also you can use UITableView instead of stack & labels
Maybe this documentation will help too (it is written that usually in one view better to keep one gesture recognizer): https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/coordinating_multiple_gesture_recognizers

UILabel skeleton not showing while it's inside a stack view

Simply, I'm trying to display the skeleton for two UILabels that are subviews of a stack view. When I'm saying label.isSkeletonable = true it doesn't work at all. However, when I make the stack view isSkeletonable = true it works and becomes like the picture below
class ArticleCellView: UITableViewCell {
// MARK: - *** Properties ***
static let cellIdentifier = "ArticleTableViewCell"
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.isSkeletonable = true
contentView.isSkeletonable = true
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - *** UI Elements ***
lazy var titleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.textAlignment = .natural
label.textColor = UIColor(named: "AccentColor")
label.font = UIFont.systemFont(ofSize: 13.0, weight: .medium)
label.isSkeletonable = true
return label
}()
lazy var abstractLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 2
label.textAlignment = .natural
label.textColor = UIColor(named: "AccentColor")
label.font = UIFont.systemFont(ofSize: 12.0)
label.isSkeletonable = true
return label
}()
lazy var thumbnailImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.frame.size.width = 100
imageView.layer.cornerRadius = imageView.frame.size.width / 2
imageView.clipsToBounds = true
imageView.isSkeletonable = true
return imageView
}()
lazy var stackView: UIStackView = {
let stack = UIStackView()
stack.contentMode = .scaleToFill
stack.distribution = .fillEqually
stack.spacing = 20
stack.isSkeletonable = true
return stack
}()
func configure() {
// Adding subviews
contentView.addSubview(thumbnailImageView)
contentView.addSubview(stackView)
stackView.addSubview(titleLabel)
stackView.addSubview(abstractLabel)
// Setting up the constraints
thumbnailImageView.snp.makeConstraints {
$0.leading.equalToSuperview().inset(10)
$0.width.height.equalTo(100)
$0.centerY.equalToSuperview()
}
stackView.snp.makeConstraints {
$0.leading.equalTo(thumbnailImageView.snp.trailing).offset(10)
$0.trailing.equalToSuperview().inset(10)
$0.top.bottom.equalTo(thumbnailImageView)
}
titleLabel.snp.makeConstraints {
$0.leading.equalToSuperview().inset(10)
$0.trailing.equalToSuperview().inset(2)
$0.top.equalToSuperview().inset(10)
}
abstractLabel.snp.makeConstraints {
$0.leading.equalToSuperview().inset(10)
$0.trailing.equalToSuperview().inset(2)
$0.top.equalTo(titleLabel.snp.bottom).offset(10)
$0.bottom.equalToSuperview().offset(2)
}
}
}
As far as you can tell, no other solution out there worked for me even clipsToBounds did nothing. I'm using the SkeletonView from the following: https://github.com/Juanpe/SkeletonView
First, you need to add the labels as arranged subviews of the stack view.
So, this:
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(abstractLabel)
instead of this:
stackView.addSubview(titleLabel)
stackView.addSubview(abstractLabel)
Next, because you want the labels to be arranged Vertically, set the stack view axis (and spacing) accordingly:
stack.axis = .vertical
stack.spacing = 10
and, because we want the stack view to arrange the labels, don't set any constraints on the labels:
// titleLabel.snp.makeConstraints {
// $0.leading.equalToSuperview().inset(10)
// $0.trailing.equalToSuperview().inset(2)
// $0.top.equalToSuperview().inset(10)
// }
//
// abstractLabel.snp.makeConstraints {
// $0.leading.equalToSuperview().inset(10)
// $0.trailing.equalToSuperview().inset(2)
// $0.top.equalTo(titleLabel.snp.bottom).offset(10)
// $0.bottom.equalToSuperview().offset(2)
// }
and finally, it is a really good idea to add comments so you understand what your code is trying to do:
// Setting up the constraints
thumbnailImageView.snp.makeConstraints {
// image view leading is 10-points from superview (contentView) leading
$0.leading.equalToSuperview().inset(10)
// 100 x 100
$0.width.height.equalTo(100)
// top and bottom constrained to top and bottom of superview (contenView)
$0.top.bottom.equalToSuperview()
}
stackView.snp.makeConstraints {
// stackView leading is 10=points from image view trailing
$0.leading.equalTo(thumbnailImageView.snp.trailing).offset(10)
// 10-points from superview (contentView) trailing
$0.trailing.equalToSuperview().inset(10)
// stackView centered vertically to image view
$0.centerY.equalTo(thumbnailImageView)
}
Here's the full, modified version of your cell class:
class ArticleCellView: UITableViewCell {
// MARK: - *** Properties ***
static let cellIdentifier = "ArticleTableViewCell"
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// from the SkeletonView docs:
// SkeletonView is recursive, so if you want show the skeleton in all skeletonable views,
// you only need to call the show method in the main container view. For example, with UIViewControllers.
// So, we only need to set it on self
self.isSkeletonable = true
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - *** UI Elements ***
lazy var titleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.textAlignment = .natural
label.textColor = UIColor(named: "AccentColor")
label.font = UIFont.systemFont(ofSize: 13.0, weight: .medium)
return label
}()
lazy var abstractLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 2
label.textAlignment = .natural
label.textColor = UIColor(named: "AccentColor")
label.font = UIFont.systemFont(ofSize: 12.0)
return label
}()
lazy var thumbnailImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.frame.size.width = 100
imageView.layer.cornerRadius = imageView.frame.size.width / 2
imageView.clipsToBounds = true
return imageView
}()
lazy var stackView: UIStackView = {
let stack = UIStackView()
stack.distribution = .fillEqually
// we want Vertical Stack View
stack.axis = .vertical
stack.spacing = 10
return stack
}()
// MARK: - *** Methods ***
override func prepareForReuse() {
super.prepareForReuse()
thumbnailImageView.kf.cancelDownloadTask()
thumbnailImageView.image = nil
}
func configure() {
// Adding subviews
contentView.addSubview(thumbnailImageView)
contentView.addSubview(stackView)
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(abstractLabel)
// Setting up the constraints
thumbnailImageView.snp.makeConstraints {
// leading is 10-points from superview (contentView) leading
$0.leading.equalToSuperview().inset(10)
// 100 x 100
$0.width.height.equalTo(100)
// top and bottom constrained to top and bottom of superview (contenView)
$0.top.bottom.equalToSuperview()
}
stackView.snp.makeConstraints {
// stackView leading is 10=points from image view trailing
$0.leading.equalTo(thumbnailImageView.snp.trailing).offset(10)
// 10-points from superview (contentView) trailing
$0.trailing.equalToSuperview().inset(10)
// stackView centered vertically to image view
$0.centerY.equalTo(thumbnailImageView)
}
}
}

How should I set constraints to the subviews of my tableHeaderView?

I have this viewController:
class CreateSkillGroupViewController: UIViewController {
lazy var headerStack: UIStackView = {
let stack = UIStackView(frame: CGRect(x: 0, y: 0, width: 20, height: 400))
stack.axis = .vertical
let titleField = UITextView(frame: CGRect(x: 0, y: 0, width: 300, height: 88))
titleField.backgroundColor = .green
titleField.snp.makeConstraints{ (make) in
make.height.equalTo(50)
}
let descriptionField = UITextView(frame: CGRect(x: 0, y: 0, width: 300, height: 120))
descriptionField.snp.makeConstraints{ (make) in
make.height.equalTo(100)
}
let headerImage = UIImageView(image: UIImage(named: "AppIcon-bw"))
headerImage.snp.makeConstraints{ (make) in
make.height.equalTo(300)
make.width.equalTo(200)
}
stack.addArrangedSubview(headerImage)
stack.addArrangedSubview(titleField)
stack.addArrangedSubview(descriptionField)
stack.backgroundColor = .blue
return stack
}()
override func viewDidLoad() {
super.viewDidLoad()
configureNavigationItem()
skillsTableView = UITableView(frame: .zero, style: .insetGrouped)
skillsTableView.register(SkillSummaryCell.self)
skillsTableView.tableHeaderView = headerStack
view.addSubview(skillsTableView)
skillsTableView.tableHeaderView?.snp.makeConstraints{ (make) in
make.top.equalToSuperview()
make.left.equalToSuperview()
make.right.equalToSuperview()
make.width.equalToSuperview()
make.height.equalTo(400)
}
skillsTableView.snp.makeConstraints{ (make) in
make.edges.equalToSuperview()
}
...
This is what it creates...
As you can see I use the lazy var headerStack to setup the tableHeaderView which is a stackView. As you can see all of the constraints in that stack view are explicit number sizes. Then in the viewDidLoad, I add the constraints for the tableView itself.
I want to know how I would for instance, center the headerImage in the viewController, or in the tableView for that matter or make its width half of the tableView's width. I cannot set equalToSuperView because the view hasn't been laid out yet. And once its laid out, I cannot access the stack view subviews to retroactively add constraints to them.
First of all, I wouldn't use a stackView as a tableHeaderView because you need your tableHeaderView to be the same width as the tableView. Embed your stackView in a view and use that view as the header. Ensure that header remains the width of the tableView regardless of the stackView content.
Also, it looks like you are trying to mix autolayout with frame-based layout and that's gonna get you into trouble. I'm not sure why you were setting frames on some of your subviews.
Pay attention to how you define stackView.alignment and stackView.distribution. I'm not sure what your goal is so it's hard to give you much advice there. Bit I assume you want your subviews centered and to have their own unique width.
You defined a lot of your subviews in your stackView builder and that got you into trouble. Ensure that you have one builder for each subview. It helps keep your code clean.
Lastly, you can use autolayout to define the width equal to the width of the tableView. There are a lot of solutions on the web that make you compute the frames for your header manually and that's just a pain.
I changed some names around added some colors but I think this will help you:
extension UIColor {
static let headerImage = UIColor.systemPurple
static let header = UIColor.systemPink
static let titleField = UIColor.white
static let descriptionField = UIColor.systemYellow
static let headerStack = UIColor.systemOrange
static let tableView = UIColor.systemMint
}
class ViewController: UIViewController {
lazy var headerImage: UIImageView = {
let headerImage = UIImageView(image: UIImage(systemName: "checkmark"))
headerImage.translatesAutoresizingMaskIntoConstraints = false
headerImage.backgroundColor = .headerImage
return headerImage
}()
lazy var headerView: UIView = {
let header = UIView()
header.backgroundColor = .header
header.translatesAutoresizingMaskIntoConstraints = false
return header
}()
lazy var titleField: UITextView = {
let titleField = UITextView(frame: .zero)
titleField.translatesAutoresizingMaskIntoConstraints = false
titleField.backgroundColor = .titleField
return titleField
}()
lazy var descriptionField: UITextView = {
let descriptionField = UITextView(frame: .zero)
descriptionField.translatesAutoresizingMaskIntoConstraints = false
descriptionField.backgroundColor = .descriptionField
return descriptionField
}()
lazy var headerStack: UIStackView = {
let stack = UIStackView(frame: .zero)
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.distribution = .fillProportionally
stack.alignment = .center
stack.spacing = 10
stack.backgroundColor = .headerStack
return stack
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .insetGrouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(SkillSummaryCell.self, forCellReuseIdentifier: "SkillSummaryCell")
tableView.backgroundColor = .tableView
tableView.delegate = self
tableView.dataSource = self
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
addViews()
arrangeViews()
tableView.layoutIfNeeded()
}
func addViews() {
view.addSubview(tableView)
headerStack.addArrangedSubview(headerImage)
headerStack.addArrangedSubview(titleField)
headerStack.addArrangedSubview(descriptionField)
headerView.addSubview(headerStack)
tableView.tableHeaderView = headerView
}
func arrangeViews() {
tableView.snp.makeConstraints{ (make) in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
descriptionField.snp.makeConstraints{ (make) in
make.height.equalTo(100)
make.width.equalTo(300)
}
titleField.snp.makeConstraints{ (make) in
make.height.equalTo(100)
make.width.equalTo(300)
}
headerStack.snp.makeConstraints { make in
make.top.equalToSuperview()
make.bottom.equalToSuperview()
make.centerX.equalToSuperview()
}
headerView.snp.makeConstraints { make in
make.width.equalTo(tableView)
}
headerImage.snp.makeConstraints{ (make) in
make.width.equalTo(tableView).dividedBy(2)
make.height.equalTo(headerImage.snp.width)
}
}
}
use it:
viewForHeaderInSection
as
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let tableBounds = tableView.bounds // <- table size
let sectionIndex = section // <- Section index
}
In this method, you can customize the header for a specific section, and take into account the size of your table
ALSO:
You can use UIScreen.main.bounds - get the screen size of your phone at any time, this can be very useful, especially considering that tables are often equal in width to the width of the screen

How to update constraints of a view when other view is hidden using Snapkit?

There are three views from top to bottom: redView, yellowView, blueView.
I want to hide yellowView and change the constraint of blueView in order to let blueView below redView.
The picture is below:
this picture is original
this picture is what I want
The code is below:
private lazy var redView: UIView = {
let redView = UIView()
redView.backgroundColor = UIColor.red
return redView
}()
private lazy var yellowView: UIView = {
let yellowView = UIView()
yellowView.backgroundColor = UIColor.yellow
return yellowView
}()
private lazy var blueView: UIView = {
let blueView = UIView()
blueView.backgroundColor = UIColor.blue
return blueView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(redView)
view.addSubview(yellowView)
view.addSubview(blueView)
redView.snp.makeConstraints { (make) in
make.top.left.right.equalTo(view)
make.height.equalTo(40)
}
yellowView.snp.makeConstraints { (make) in
make.top.equalTo(redView.snp.bottom)
make.left.right.height.equalTo(redView)
}
blueView.snp.makeConstraints { (make) in
make.top.equalTo(yellowView.snp.bottom)
make.left.right.height.equalTo(yellowView)
}
}
if yellowView.isHidden == true {
//how is the code?
} else {
//how is the code?
}
You need set height constraint for yellow view and take IBOutlet for its height constraint of yellowView and modify its constant as per hide/show .
if yellowView.isHidden == true {
//how is the code?
ibHeightOutletOfYellow.constant = 0; // hide here
} else {
//how is the code?
ibHeightOutletOfYellow.constant = 50; // as per your needed
}

How to change the background color of UIStackView?

I tried to change the UIStackView background from clear to white in Storyboard inspector, but when simulating, the background color of the stack view still has a clear color.
How can I change the background color of a UIStackView?
You can't do this – UIStackView is a non-drawing view, meaning that
drawRect() is never called and its background color is ignored. If you
desperately want a background color, consider placing the stack view
inside another UIView and giving that view a background color.
Reference from HERE.
EDIT:
You can add a subView to UIStackView as mentioned HERE or in this answer (below) and assign a color to it. Check out below extension for that:
extension UIStackView {
func addBackground(color: UIColor) {
let subView = UIView(frame: bounds)
subView.backgroundColor = color
subView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
insertSubview(subView, at: 0)
}
}
And you can use it like:
stackView.addBackground(color: .red)
I do it like this:
#IBDesignable
class StackView: UIStackView {
#IBInspectable private var color: UIColor?
override var backgroundColor: UIColor? {
get { return color }
set {
color = newValue
self.setNeedsLayout() // EDIT 2017-02-03 thank you #BruceLiu
}
}
private lazy var backgroundLayer: CAShapeLayer = {
let layer = CAShapeLayer()
self.layer.insertSublayer(layer, at: 0)
return layer
}()
override func layoutSubviews() {
super.layoutSubviews()
backgroundLayer.path = UIBezierPath(rect: self.bounds).cgPath
backgroundLayer.fillColor = self.backgroundColor?.cgColor
}
}
Works like a charm
UIStackView is a non-rendering element, and as such, it does not get drawn on the screen. This means that changing backgroundColor essentially does nothing. If you want to change the background color, just add a UIView to it as a subview (that is not arranged) like below:
extension UIStackView {
func addBackground(color: UIColor) {
let subview = UIView(frame: bounds)
subview.backgroundColor = color
subview.autoresizingMask = [.flexibleWidth, .flexibleHeight]
insertSubview(subview, at: 0)
}
}
It's worth pointing out that starting with iOS 14, UIStackViews do render background colours. You can either set the background of the UIStackView from the Storyboard with the Background property.
Or in code with:
if #available(iOS 14.0, *) {
stackView.backgroundColor = .green
} else {
// Fallback for older versions of iOS
}
Maybe the easiest, more readable and less hacky way would be to embed the UIStackView into a UIView and set the background color to the view.
And don't forget to configure properly the Auto Layout constraints between those two views… ;-)
Pitiphong is correct, to get a stackview with a background color do something like the following...
let bg = UIView(frame: stackView.bounds)
bg.autoresizingMask = [.flexibleWidth, .flexibleHeight]
bg.backgroundColor = UIColor.red
stackView.insertSubview(bg, at: 0)
This will give you a stackview whose contents will be placed on a red background.
To add padding to the stackview so the contents aren't flush with the edges, add the following in code or on the storyboard...
stackView.isLayoutMarginsRelativeArrangement = true
stackView.layoutMargins = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
TL;DR: The official way to do this is by adding an empty view into stack view using addSubview: method and set the added view background instead.
The explanation: UIStackView is a special UIView subclass that only do the layout not drawing. So many of its properties won't work as usual. And since UIStackView will layout its arranged subviews only, this mean that you can simply add it a UIView with addSubview: method, set its constraints and background color. This is the official way to achieve what you want quoted from WWDC session
This works for me in Swift 3 and iOS 10:
let stackView = UIStackView()
let subView = UIView()
subView.backgroundColor = .red
subView.translatesAutoresizingMaskIntoConstraints = false
stackView.addSubview(subView) // Important: addSubview() not addArrangedSubview()
// use whatever constraint method you like to
// constrain subView to the size of stackView.
subView.topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
subView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
subView.leftAnchor.constraint(equalTo: stackView.leftAnchor).isActive = true
subView.rightAnchor.constraint(equalTo: stackView.rightAnchor).isActive = true
// now add your arranged subViews...
stackView.addArrangedSubview(button1)
stackView.addArrangedSubview(button2)
Here is a brief overview for adding a Stack view Background Color.
class RevealViewController: UIViewController {
#IBOutlet private weak var rootStackView: UIStackView!
Creating background view with rounded corners
private lazy var backgroundView: UIView = {
let view = UIView()
view.backgroundColor = .purple
view.layer.cornerRadius = 10.0
return view
}()
To make it appear as the background we add it to the subviews array of the root stack view at index 0. That puts it behind the arranged views of the stack view.
private func pinBackground(_ view: UIView, to stackView: UIStackView) {
view.translatesAutoresizingMaskIntoConstraints = false
stackView.insertSubview(view, at: 0)
view.pin(to: stackView)
}
Add constraints to pin the backgroundView to the edges of the stack view, by using a small extension on UIView.
public extension UIView {
public func pin(to view: UIView) {
NSLayoutConstraint.activate([
leadingAnchor.constraint(equalTo: view.leadingAnchor),
trailingAnchor.constraint(equalTo: view.trailingAnchor),
topAnchor.constraint(equalTo: view.topAnchor),
bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
call the pinBackground from viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
pinBackground(backgroundView, to: rootStackView)
}
Reference from: HERE
In iOS10, #Arbitur's answer needs a setNeedsLayout after color is set. This is the change which is needed:
override var backgroundColor: UIColor? {
get { return color }
set {
color = newValue
setNeedsLayout()
}
}
Xamarin, C# version:
var stackView = new UIStackView { Axis = UILayoutConstraintAxis.Vertical };
UIView bg = new UIView(stackView.Bounds);
bg.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
bg.BackgroundColor = UIColor.White;
stackView.AddSubview(bg);
You could make a small extension of UIStackView
extension UIStackView {
func setBackgroundColor(_ color: UIColor) {
let backgroundView = UIView(frame: .zero)
backgroundView.backgroundColor = color
backgroundView.translatesAutoresizingMaskIntoConstraints = false
self.insertSubview(backgroundView, at: 0)
NSLayoutConstraint.activate([
backgroundView.topAnchor.constraint(equalTo: self.topAnchor),
backgroundView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
backgroundView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
backgroundView.trailingAnchor.constraint(equalTo: self.trailingAnchor)
])
}
}
Usage:
yourStackView.setBackgroundColor(.black)
UIStackView *stackView;
UIView *stackBkg = [[UIView alloc] initWithFrame:CGRectZero];
stackBkg.backgroundColor = [UIColor redColor];
[self.view insertSubview:stackBkg belowSubview:stackView];
stackBkg.translatesAutoresizingMaskIntoConstraints = NO;
[[stackBkg.topAnchor constraintEqualToAnchor:stackView.topAnchor] setActive:YES];
[[stackBkg.bottomAnchor constraintEqualToAnchor:stackView.bottomAnchor] setActive:YES];
[[stackBkg.leftAnchor constraintEqualToAnchor:stackView.leftAnchor] setActive:YES];
[[stackBkg.rightAnchor constraintEqualToAnchor:stackView.rightAnchor] setActive:YES];
Subclass UIStackView
class CustomStackView : UIStackView {
private var _bkgColor: UIColor?
override public var backgroundColor: UIColor? {
get { return _bkgColor }
set {
_bkgColor = newValue
setNeedsLayout()
}
}
private lazy var backgroundLayer: CAShapeLayer = {
let layer = CAShapeLayer()
self.layer.insertSublayer(layer, at: 0)
return layer
}()
override public func layoutSubviews() {
super.layoutSubviews()
backgroundLayer.path = UIBezierPath(rect: self.bounds).cgPath
backgroundLayer.fillColor = self.backgroundColor?.cgColor
}
}
Then in your class
yourStackView.backgroundColor = UIColor.lightGray
You can insert a sublayer to StackView, it works to me:
#interface StackView ()
#property (nonatomic, strong, nonnull) CALayer *ly;
#end
#implementation StackView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_ly = [CALayer new];
[self.layer addSublayer:_ly];
}
return self;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.ly.backgroundColor = backgroundColor.CGColor;
}
- (void)layoutSubviews {
self.ly.frame = self.bounds;
[super layoutSubviews];
}
#end
I am little bit sceptical in Subclassing UI components. This is how I am using it,
struct CustomAttributeNames{
static var _backgroundView = "_backgroundView"
}
extension UIStackView{
var backgroundView:UIView {
get {
if let view = objc_getAssociatedObject(self, &CustomAttributeNames._backgroundView) as? UIView {
return view
}
//Create and add
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
insertSubview(view, at: 0)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: self.topAnchor),
view.leadingAnchor.constraint(equalTo: self.leadingAnchor),
view.bottomAnchor.constraint(equalTo: self.bottomAnchor),
view.trailingAnchor.constraint(equalTo: self.trailingAnchor)
])
objc_setAssociatedObject(self,
&CustomAttributeNames._backgroundView,
view,
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return view
}
}
}
And this is the usage,
stackView.backgroundView.backgroundColor = .white
stackView.backgroundView.layer.borderWidth = 2.0
stackView.backgroundView.layer.borderColor = UIColor.red.cgColor
stackView.backgroundView.layer.cornerRadius = 4.0
Note: With this approach, if you want to set border, you have to set layoutMargins on the stackView so that the border is visible.
You can't add background to stackview.
But what you can do is adding stackview in a view and then set background of view this will get the job done.
*It will not gonna interrupt the flows of stackview.
Hope this will help.
We can have a custom class StackView like this:
class StackView: UIStackView {
lazy var backgroundView: UIView = {
let otherView = UIView()
addPinedSubview(otherView)
return otherView
}()
}
extension UIView {
func addPinedSubview(_ otherView: UIView) {
addSubview(otherView)
otherView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
otherView.trailingAnchor.constraint(equalTo: trailingAnchor),
otherView.topAnchor.constraint(equalTo: topAnchor),
otherView.heightAnchor.constraint(equalTo: heightAnchor),
otherView.widthAnchor.constraint(equalTo: widthAnchor),
])
}
}
And it can be used like this:
let stackView = StackView()
stackView.backgroundView.backgroundColor = UIColor.lightGray
This is slightly better than adding an extension function func addBackground(color: UIColor) as suggested by others. The background view is lazy so that it won't be created until you actually call stackView.backgroundView.backgroundColor = .... And setting/changing the background color multiple times won't result in multiple subviews being inserted in the stack view.
If you want to control from designer itself , add this extension to stack view
#IBInspectable var customBackgroundColor: UIColor?{
get{
return backgroundColor
}
set{
backgroundColor = newValue
let subview = UIView(frame: bounds)
subview.backgroundColor = newValue
subview.autoresizingMask = [.flexibleWidth, .flexibleHeight]
insertSubview(subview, at: 0)
}
}
There's good answers but i found them not complete so here is my version based on best of them:
/// This extension addes missing background color to stack views on iOS 13 and earlier
extension UIStackView {
private struct CustomAttributeNames {
static var _backgroundView = "_backgroundView"
}
#IBInspectable var customBackgroundColor: UIColor? {
get { backgroundColor }
set { setBackgroundColor(newValue) }
}
var backgroundView: UIView {
if let view = objc_getAssociatedObject(self, &CustomAttributeNames._backgroundView) as? UIView {
return view
}
let view = UIView(frame: bounds)
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
insertSubview(view, at: 0)
objc_setAssociatedObject(self,
&CustomAttributeNames._backgroundView,
view,
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return view
}
func setBackgroundColor(_ color: UIColor?) {
backgroundColor = color
backgroundView.backgroundColor = color
}
}
You could do it like this:
stackView.backgroundColor = UIColor.blue
By providing an extension to override the backgroundColor:
extension UIStackView {
override open var backgroundColor: UIColor? {
get {
return super.backgroundColor
}
set {
super.backgroundColor = newValue
let tag = -9999
for view in subviews where view.tag == tag {
view.removeFromSuperview()
}
let subView = UIView()
subView.tag = tag
subView.backgroundColor = newValue
subView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(subView)
subView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
subView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
subView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
subView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
}
}
}
The explanation from the Apple documentation is that a stack view is never itself rendered in iOS 13 - it’s purpose is to manage its arranged subviews:
The UIStackView is a nonrendering subclass of UIView; that is, it does not provide any user interface of its own. Instead, it just manages the position and size of its arranged views. As a result, some properties (like backgroundColor) have no effect on the stack view.
You could fix this by creating an extension just for fixing the background color in iOS 13 or below:
import UIKit
extension UIStackView {
// MARK: Stored properties
private enum Keys {
static var backgroundColorView: UInt8 = 0
}
private var backgroundColorView: UIView? {
get {
objc_getAssociatedObject(self, &Keys.backgroundColorView) as? UIView
}
set {
objc_setAssociatedObject(self, &Keys.backgroundColorView, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
override open var backgroundColor: UIColor? {
didSet {
// UIKit already support setting background color in iOS 14 or above
guard #available(iOS 14.0, *) else {
// fix setting background color directly to stackview by add a background view
if backgroundColorView == nil {
let backgroundColorView = UIView(frame: bounds)
backgroundColorView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
insertSubview(backgroundColorView, at: 0)
self.backgroundColorView = backgroundColorView
}
backgroundColorView?.backgroundColor = backgroundColor
return
}
}
}
}

Resources