UIKit UIStackView with auto width - ios

I'm making an app with UIkit and I'm making some UITableViewCells that contain an array of user images. I want these images to be displayed in a horizontal stack and overlay each other.
This is how I want it to look:
That's how it looks:
Code:
import UIKit
import SDWebImage
class CountryTableCell: UITableViewCell {
static let reuseIdentifier = "CountryTableCell"
//MARK: - Propeties
var viewModel: CountryViewModel? {
didSet {
viewModel?.delegate = self
setUpSpace(viewModel)
}
}
//MARK: - SubViews
private let container: UIView = {
let view = UIView()
view.withSize(CGSize(width: Dimensions.maxSafeWidth, height: 150))
view.backgroundColor = .secondary_background
view.layer.cornerRadius = 20
return view
}()
private let adminsContainer: UIView = {
let view = UIView()
view.withSize(CGSize(width: (Dimensions.maxSafeWidth - 32), height: Dimensions.image.mediumHeigthWithOverlay))
view.backgroundColor = .secondary_background
return view
}()
private let adminsStack: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .leading
stack.spacing = -10
stack.sizeToFit()
stack.distribution = .fillEqually
stack.withSize(CGSize(width: Dimensions.image.mediumHeigthWithOverlay, height: Dimensions.image.mediumHeigthWithOverlay))
return stack
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: UIFont.preferredFont(forTextStyle: .title2).pointSize, weight: .bold)
label.textColor = .primary_label
label.textAlignment = .left
label.sizeToFit()
label.numberOfLines = 0
return label
}()
var headerView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
iv.withSize(CGSize(width: Dimensions.image.headerSizeForCell.width, height: ((Dimensions.image.mediumHeigth/2) + Padding.horizontal)))
iv.backgroundColor = .secondary_background
iv.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
iv.layer.cornerRadius = 20
return iv
}()
//MARK: - Init
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(container)
container.center(inView: self)
addSubview(headerView)
headerView.anchor(top: container.topAnchor, left: container.leftAnchor, right: container.rightAnchor)
addSubview(nameLabel)
nameLabel.anchor(top: headerView.bottomAnchor, left: container.leftAnchor, right: container.rightAnchor, paddingLeft: Padding.horizontal, paddingRight: Padding.horizontal)
addSubview(adminsContainer)
adminsContainer.anchor(left: container.leftAnchor, bottom: headerView.bottomAnchor, right: container.rightAnchor ,paddingLeft: Padding.horizontal, paddingBottom: -(Dimensions.image.mediumHeigth/2), paddingRight: Padding.horizontal)
bringSubviewToFront(adminsContainer)
adminsContainer.backgroundColor = .clear.withAlphaComponent(0.0)
adminsContainer.addSubview(adminsStack)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Lifecycle
override func willRemoveSubview(_ subview: UIView) {
}
//MARK: Selectors
//MARK: - Helpers
private func setUpSpace(_ viewModel: CountryViewModel?) {
guard let viewModel = viewModel else {return}
if let url = viewModel.country.headerImage?.url {
headerView.sd_setImage(with: URL(string: url))
}
nameLabel.text = viewModel.space.name
}
}
// MARK: Extension
extension SpaceTableCell: CountryViewModelDelegate {
func didFetchAdmin(_ admin: User) {
if let admins = viewModel?.admins.count {
if admins == 1 {
adminsStack.withSize(CGSize(width: Dimensions.image.mediumHeigthWithOverlay, height: Dimensions.image.mediumHeigthWithOverlay))
} else if admins == 2 {
adminsStack.withSize(CGSize(width: ((Dimensions.image.mediumHeigthWithOverlay*2) - 10), height: Dimensions.image.mediumHeigthWithOverlay))
} else if admins == 3 {
adminsStack.withSize(CGSize(width: ((Dimensions.image.mediumHeigthWithOverlay*3) - 20), height: Dimensions.image.mediumHeigthWithOverlay))
} else if admins == 4 {
adminsStack.withSize(CGSize(width: ((Dimensions.image.mediumHeigthWithOverlay*4) - 30), height: Dimensions.image.mediumHeigthWithOverlay))
} else if admins == 5 {
adminsStack.withSize(CGSize(width: ((Dimensions.image.mediumHeigthWithOverlay*5) - 40), height: Dimensions.image.mediumHeigthWithOverlay))
}
}
let image = UserImageView(height: Dimensions.image.mediumHeigth)
image.sd_setImage(with: admin.profileImageURL)
adminsStack.addArrangedSubview(image)
}
}
class UserImageView: UIImageView {
//MARK: - Propeties
let selectedHeigth: CGFloat
init(height: CGFloat) {
self.selectedHeigth = height
super.init(frame: CGRect(x: 0, y: 0, width: selectedHeigth, height: selectedHeigth))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
contentMode = .scaleAspectFill
clipsToBounds = true
backgroundColor = .secondary_background
layer.cornerRadius = Dimensions.userImageCornerRadious(selectedHeigth)
}
}
Can someone please help me
Thank you :)

I'd suggest a couple changes:
Change #1:
Your UIStackView's distribution set to .fillEqually. By doing this, you're giving it the permission to stretch its arrangedSubviews as and when needed to fit the width that you've provided. This does prove useful in some cases but in your case, it's better to go with .fill instead
Here's an excerpt from Apple's documentation:
For all distributions except the UIStackView.Distribution.fillEqually
distribution, the stack view uses each arranged view’s
intrinsicContentSize property when calculating its size along the
stack’s axis. UIStackView.Distribution.fillEqually resizes all the
arranged views so they’re the same size, filling the stack view along
its axis. If possible, the stack view stretches all the arranged views
to match the view with the longest intrinsic size along the stack’s
axis.
I suggest that you go through this documentation and read more about this
Change #2:
Since you've now changed the distribution to respect intrinsic size, don't set up constraints that conflict with UIStackView's distribution axis. In your case, you have a horizontal stack view and seem to be also defining its width constraints based on the number of admins you have. I'd suggest getting rid of it and leave it to your stack view to take care of it.
One more pointer:
Make sure your UserImageViews have proper unified intrinsicContentSize i.e: the images you set follow the same size. If doubtful, I'd suggest setting up constraints for its width and height respectively. To do so, in your UserImageView's init, include:
self.widthAnchor.constraint(equalToConstant: height).isActive = true
self.heightAnchor.constraint(equalToConstant: height).isActive = true

Related

Subviews of custom cell class not recognizing tap gesture

So I have a custom cell class sitting in my table view with stack views that contain UIImageViews. I want to add tap gesture recognition to the symbols, however the gesture isn't recognized for ONLY subviews of the custom cell (it works fine for the view itself). My cell code looks like:
class MonthlyViewCell: UITableViewCell {
private let someSymbol: UIImageView = {
guard let image = UIImage(systemName: "j.circle.fill") else {return nil}
let config = UIImage.SymbolConfiguration(pointSize: 27)
let newImage = image.applyingSymbolConfiguration(config)
let view = UIImageView(image: newImage)
view.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
view.contentMode = .scaleAspectFit
view.tag = 1
view.isUserInteractionEnabled = true
return view
}()
.
.
// other symbols
private var delegate: MonthlyViewCellDelegate?
private var stackViewOne = UIStackView()
private var stackViewTwo = UIStackView()
private var stackViewThree = UIStackView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func commonInit() {
bounds = CGRect(x: 0, y: 0, width: 320, height: 120)
frame = bounds
print(frame)
configureSubViews()
addSubview(stackViewOne)
addSubview(stackViewTwo)
addSubview(stackViewThree)
//addTargetToSymbols()
activateConstraints()
stackViewOne.frame = CGRect(x: 0, y: 0, width: frame.width-1, height: 15)
stackViewTwo.frame = CGRect(x: 0, y: 0, width: frame.width-1, height: 15)
stackViewThree.frame = CGRect(x: 0, y: 0, width: frame.width-1, height: 15)
let gestureRecognizer = UITapGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(testTapped(sender:)))
stackViewTwo.addGestureRecognizer(gestureRecognizer)
}
private func configureSubViews() {
stackViewOne.translatesAutoresizingMaskIntoConstraints = false
stackViewTwo.translatesAutoresizingMaskIntoConstraints = false
stackViewThree.translatesAutoresizingMaskIntoConstraints = false
stackViewOne.axis = .horizontal
stackViewTwo.axis = .horizontal
stackViewThree.axis = .horizontal
stackViewOne.spacing = 60
stackViewTwo.spacing = 60
stackViewThree.spacing = 60
let viewsOne = [januarySymbol, februarySymbol, marchSymbol, aprilSymbol]
let viewsTwo = [maySymbol, juneSymbol, julySymbol, augustSymbol]
let viewsThree = [septemberSymbol, octoberSymbol, novemberSymbol, decemberSymbol]
let stackViews = [stackViewOne,stackViewTwo,stackViewThree]
for one in viewsOne {
stackViewOne.addArrangedSubview(one!)
}
for two in viewsTwo {
stackViewTwo.addArrangedSubview(two!)
}
for three in viewsThree {
stackViewThree.addArrangedSubview(three!)
}
/*for view in stackViews {
bringSubviewToFront(view)
}*/
}
private func activateConstraints() {
let array: [NSLayoutConstraint] = [
stackViewOne.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
stackViewTwo.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
stackViewThree.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
stackViewOne.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 25),
stackViewTwo.topAnchor.constraint(equalTo: stackViewOne.bottomAnchor, constant: 25),
stackViewThree.topAnchor.constraint(equalTo: stackViewTwo.bottomAnchor, constant: 25)
]
NSLayoutConstraint.activate(array)
}
private func addTargetToSymbols() {
let gestureRecognizer = UITapGestureRecognizer()
let symbols: [UIImageView?] = [januarySymbol, februarySymbol, marchSymbol, aprilSymbol, maySymbol, juneSymbol, julySymbol, augustSymbol, septemberSymbol, novemberSymbol, decemberSymbol]
for symbol in symbols {
guard let symbol = symbol else {return}
gestureRecognizer.addTarget(self, action: #selector(monthSymbolTapped(sender:)))
symbol.addGestureRecognizer(gestureRecognizer)
bringSubviewToFront(symbol)
}
}
#objc func testTapped(sender: UITapGestureRecognizer) {
print("tapped")
}
func setDelegate(delegate: MonthlyViewCellDelegate) {
self.delegate = delegate
}
}
And the delegate methods are in the vc. I have tried setting the frames of each subview to be contained within the frame of their superviews. I should also mention that I set a breakpoint at the target methods to be called and none of them are triggered, so it seems that the methods aren't being called.
Also isUserInteractionEnabled is set to true for the view and its subviews. translatesAutoresizingMaskIntoConstraints is set to false for all subviews but NOT for the view itself, as this conflicts with the views auto constraints with the tableview it is contained in.
One possibility is it may have something to do with the constraints I set causing the stackviews respective frames to exceed the frame of its superview, however I set the stackviews's frames AFTER the constraints are activated, so this seems unlikely as well.
Finally I should mention that that the UIViewController has its view assigned to a .UITableView property which contains the cell which is initialized in cellForRowAt.
EDIT: As mentioned in the comments it is possible that using only one UITapGestureRecognizer instance in addTargetsToSymbols()for multiple subviews was the issue, so I made the following adjustment;
private func addTargetToSymbols() {
let symbols: [UIImageView?] = [januarySymbol, februarySymbol, marchSymbol, aprilSymbol, maySymbol, juneSymbol, julySymbol, augustSymbol, septemberSymbol, novemberSymbol, decemberSymbol]
for symbol in symbols {
guard let symbol = symbol else {return}
let gestureRecognizer = UITapGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(monthSymbolTapped(sender:)))
symbol.addGestureRecognizer(gestureRecognizer)
bringSubviewToFront(symbol)
}
}
moving the instance into the for-in loop, so that a new unique instance is used for each loop. This did not work. Additionally I imagine my attempt to test this on stackViewTwo individually would've worked if it was true.
As requested in the comments; here is the code for monthSymbolTapped()
#objc func monthSymbolTapped(sender: UIGestureRecognizer) {
print("tst")
guard let tag = sender.view?.tag else {return}
switch tag {
case 1:
delegate?.pushToYearlyVisits(month: tag)
// other cases
default:
return
}
}
NOTE: addTargetsToSymbols() was commented out while trying to see if simply adding gesture recognition to one of the stack views would yield a different result. Otherwise addTargetsToSymbols() has not been commented out on my local machine when troubleshooting this problem.
Here is a version of your custom cell class that works just fine with each image view having its own tap gesture.
I cleaned up the code a lot. I created a main stack view that contains the 3 stack views you had with each of those containing 4 of the image views.
I eliminated most of the attempts to set frames since constraints are being used.
I think your main issue was having incorrect constraints for the stack views which resulted in most of the image views being outside of the frame's cell which prevented the gestures from working. You should also be adding subviews to the cell's contentView, not the cell itself.
I also changed how the image views are created so I could get a fully working demo since your code was not complete. Obviously you can use your own code for all of the symbols if you prefer.
With the updated code below, tapping on any of the 12 images views results in the "tapped" message being printed.
You should also set your table view's rowHeight property to UITableView.automaticDimension to ensure the correct row height.
class MonthlyViewCell: UITableViewCell {
private static func monthImage(_ letter: String) -> UIImageView {
let image = UIImage(systemName: "\(letter).circle.fill")!
let config = UIImage.SymbolConfiguration(pointSize: 27)
let newImage = image.applyingSymbolConfiguration(config)
let view = UIImageView(image: image)
view.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
view.contentMode = .scaleAspectFit
view.isUserInteractionEnabled = true
return view
}
private let months: [UIImageView] = ["j", "f", "m", "a", "m", "j", "j", "a", "s", "o", "n", "d"].map { MonthlyViewCell.monthImage($0) }
private var mainStack = UIStackView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func commonInit() {
configureSubViews()
contentView.addSubview(mainStack)
addTargetToSymbols()
activateConstraints()
}
private func configureSubViews() {
mainStack.axis = .vertical
mainStack.distribution = .equalCentering
mainStack.alignment = .fill
mainStack.spacing = 40
mainStack.translatesAutoresizingMaskIntoConstraints = false
for i in stride(from: 0, to: 12, by: 4) {
let stack = UIStackView(arrangedSubviews: Array(months[i..<i+4]))
stack.axis = .horizontal
stack.distribution = .equalSpacing
stack.alignment = .top
stack.spacing = 60
mainStack.addArrangedSubview(stack)
}
}
private func activateConstraints() {
NSLayoutConstraint.activate([
mainStack.topAnchor.constraint(equalTo: contentView.topAnchor),
mainStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
mainStack.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 25),
mainStack.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -25),
])
}
private func addTargetToSymbols() {
for symbol in months {
let gestureRecognizer = UITapGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(monthSymbolTapped(sender:)))
symbol.addGestureRecognizer(gestureRecognizer)
}
}
#objc func monthSymbolTapped(sender: UITapGestureRecognizer) {
if sender.state == .ended {
print("tapped")
}
}
}

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

Why my 2D UIViews don't appear on screen?

I'm trying to make UIView that contains 12x7 UIViews with margins. I thought that the best way gonna be make 7 Vertical Stacks and then add all them on one big Horizontal stack. And I coded it, but problem is that this Horizontal Stacks doesn't appear on the screen at all (I've tried Xcode feature to see layers there is nothing).
This is my code:
import UIKit
class CalendarView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
// array to add in future in columnsStackView
var columnStacks: [UIStackView] = []
for columns in 1...12 {
// array to add in future in columnStackView
var columnViews: [UIView] = []
for cell in 1...7 {
let cellView = UIView(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
cellView.backgroundColor = .orange
columnViews.append(cellView)
}
// create columnStackView and add all 7 views
let columnStackView = UIStackView(arrangedSubviews: columnViews)
columnStackView.axis = .vertical
columnStackView.distribution = .fillEqually
columnStackView.alignment = .fill
columnStackView.spacing = 4
columnStacks.append(columnStackView)
}
// create columnsStackView and add those 12 stacks
let columnsStackView = UIStackView(arrangedSubviews: columnStacks)
columnsStackView.axis = .horizontal
columnsStackView.distribution = .fillEqually
columnsStackView.alignment = .fill
columnsStackView.spacing = 4
columnsStackView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(columnsStackView)
}
}
Can you please help me with that!!!
Couple things...
A UIStackView uses auto-layout when arranging its subviews, so this line:
let cellView = UIView(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
will create a UIView, but the width and height will be ignored.
You need to set those with constraints:
for cell in 1...7 {
let cellView = UIView()
cellView.backgroundColor = .orange
// we want each "cellView" to be 24x24 points
cellView.widthAnchor.constraint(equalToConstant: 24.0).isActive = true
cellView.heightAnchor.constraint(equalTo: cellView.widthAnchor).isActive = true
columnViews.append(cellView)
}
Now, because we've explicitly set the width and height of the "cellViews" we can set the stack view .distribution = .fill (instead of .fillEqually).
Next, we have to constrain the "outer" stack view (columnsStackView) to the view itself:
// constrain the "outer" stack view to self
NSLayoutConstraint.activate([
columnsStackView.topAnchor.constraint(equalTo: topAnchor),
columnsStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
columnsStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
columnsStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
otherwise, the view will have 0x0 dimensions.
Here is a modified version of your class:
class CalendarView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
// array to add in future in columnsStackView
var columnStacks: [UIStackView] = []
for columns in 1...12 {
// array to add in future in columnStackView
var columnViews: [UIView] = []
for cell in 1...7 {
let cellView = UIView()
cellView.backgroundColor = .orange
// we want each "cellView" to be 24x24 points
cellView.widthAnchor.constraint(equalToConstant: 24.0).isActive = true
cellView.heightAnchor.constraint(equalTo: cellView.widthAnchor).isActive = true
columnViews.append(cellView)
}
// create columnStackView and add all 7 views
let columnStackView = UIStackView(arrangedSubviews: columnViews)
columnStackView.axis = .vertical
columnStackView.distribution = .fill
columnStackView.alignment = .fill
columnStackView.spacing = 4
columnStacks.append(columnStackView)
}
// create columnsStackView and add those 12 stacks
let columnsStackView = UIStackView(arrangedSubviews: columnStacks)
columnsStackView.axis = .horizontal
columnsStackView.distribution = .fill
columnsStackView.alignment = .fill
columnsStackView.spacing = 4
columnsStackView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(columnsStackView)
// constrain the "outer" stack view to self
NSLayoutConstraint.activate([
columnsStackView.topAnchor.constraint(equalTo: topAnchor),
columnsStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
columnsStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
columnsStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
}
and a simple test controller to show how it can be used:
class CalendarTestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let cv = CalendarView()
cv.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(cv)
// the CalendarView will size itself, so we only need to
// provide x and y position constraints
NSLayoutConstraint.activate([
cv.centerXAnchor.constraint(equalTo: view.centerXAnchor),
cv.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
// let's give it a background color so we can see its frame
cv.backgroundColor = .systemYellow
}
}
the result:

inputAccessoryView, API error? _UIKBCompatInputView? UIViewNoIntrinsicMetric, simple code, can't figure out

Help me in one of the two ways maybe:
How to solve the problem? or
How to understand the error message?
Project summary
So I'm learning about inputAccessoryView by making a tiny project, which has only one UIButton. Tapping the button summons the keyboard with inputAccessoryView which contains 1 UITextField and 1 UIButton. The UITextField in the inputAccessoryView will be the final firstResponder that is responsible for the keyboard with that inputAccessoryView
The error message
API error: <_UIKBCompatInputView: 0x7fcefb418290; frame = (0 0; 0 0); layer = <CALayer: 0x60000295a5e0>> returned 0 width, assuming UIViewNoIntrinsicMetric
The code
is very straightforward as below
The custom UIView is used as inputAccessoryView. It installs 2 UI outlets, and tell responder chain that it canBecomeFirstResponder.
class CustomTextFieldView: UIView {
let doneButton:UIButton = {
let button = UIButton(type: .close)
return button
}()
let textField:UITextField = {
let textField = UITextField()
textField.placeholder = "placeholder"
return textField
}()
required init?(coder: NSCoder) {
super.init(coder: coder)
initSetup()
}
override init(frame:CGRect) {
super.init(frame: frame)
initSetup()
}
convenience init() {
self.init(frame: .zero)
}
func initSetup() {
addSubview(doneButton)
addSubview(textField)
}
func autosizing(to vc: UIViewController) {
frame = CGRect(x: 0, y: 0, width: vc.view.frame.size.width, height: 40)
let totalWidth = frame.size.width - 40
doneButton.frame = CGRect(x: totalWidth * 4 / 5 + 20,
y: 0,
width: totalWidth / 5,
height: frame.size.height)
textField.frame = CGRect(x: 20,
y: 0,
width: totalWidth * 4 / 5,
height: frame.size.height)
}
override var canBecomeFirstResponder: Bool { true }
override var intrinsicContentSize: CGSize {
CGSize(width: 400, height: 40)
} // overriding this variable seems to have no effect.
}
Main VC uses the custom UIView as inputAccessoryView. The UITextField in the inputAccessoryView becomes the real firstResponder in the end, I believe.
class ViewController: UIViewController {
let customView = CustomTextFieldView()
var keyboardShown = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
customView.autosizing(to: self)
}
#IBAction func summonKeyboard() {
print("hello")
keyboardShown = true
self.becomeFirstResponder()
customView.textField.becomeFirstResponder()
}
override var canBecomeFirstResponder: Bool { keyboardShown }
override var inputAccessoryView: UIView? {
return customView
}
}
I've seen people on the internet says this error message will go away if I run on a physical phone. I didn't go away when I tried.
I override intrinsicContentSize of the custom view, but it has no effect.
The error message shows twice together when I tap summon.
What "frame" or "layer" does the error message refer to? Does it refer to the custom view's frame and layer?
If we use Debug View Hierarchy we can see that _UIKBCompatInputView is part of the (internal) view hierarchy of the keyboard.
It's not unusual to see constraint errors / warnings with internal views.
Since frame and/or intrinsic content size seem to have no effect, I don't think it can be avoided (nor does it seem to need to be).
As a side note, you can keep the "Done" button round by using auto-layout constraints. Here's an example:
class CustomTextFieldView: UIView {
let textField: UITextField = {
let tf = UITextField()
tf.font = .systemFont(ofSize: 16)
tf.autocorrectionType = .no
tf.returnKeyType = .done
tf.placeholder = "placeholder"
// textField backgroundColor so we can see its frame
tf.backgroundColor = .yellow
return tf
}()
let doneButton:UIButton = {
let button = UIButton(type: .close)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
autoresizingMask = [.flexibleHeight, .flexibleWidth]
[doneButton, textField].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
}
NSLayoutConstraint.activate([
// constrain doneButton
// Trailing: 20-pts from trailing
doneButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
// Top and Bottom 8-pts from top and bottom
doneButton.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
doneButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
// Width equal to default height
// this will keep the button round instead of oval
doneButton.widthAnchor.constraint(equalTo: doneButton.heightAnchor),
// constrain textField
// Leading: 20-pts from leading
textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
// Trailing: 8-pts from doneButton leading
textField.trailingAnchor.constraint(equalTo: doneButton.leadingAnchor, constant: -8.0),
// vertically centered
textField.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}
class CustomTextFieldViewController: UIViewController {
let customView = CustomTextFieldView()
var keyboardShown = false
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func summonKeyboard() {
print("hello")
keyboardShown = true
self.becomeFirstResponder()
customView.textField.becomeFirstResponder()
}
override var canBecomeFirstResponder: Bool { keyboardShown }
override var inputAccessoryView: UIView? {
return customView
}
}

Custom UITextField with UILabel multiline support for error text

I want to create custom UITextField with error label on bottom of it. I want the label to be multiline, I tried numberOfLines = 0. But it is not working.
Here is my snippet for the class
public class MyTextField: UITextField {
private let helperTextLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 12.0, weight: UIFont.Weight.regular)
label.textColor = helperTextColor
label.numberOfLines = 0
return label
}()
public override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(errorTextLabel)
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addSubview(errorTextLabel)
}
public override func layoutSubviews() {
super.layoutSubviews()
errorTextLabel.frame = CGRect(x: bounds.minX, y: bounds.maxY - 20, width: bounds.width, height: 20)
}
public override var intrinsicContentSize: CGSize {
return CGSize(width: 240.0, height: 68.0)
}
public override func sizeThatFits(_ size: CGSize) -> CGSize {
return intrinsicContentSize
}
}
I think the root cause is because I set height to 20, but how can I set the height dynamically based on the errorTextLabel.text value?
You are giving your label a fixed size.
Not using AutoLayout and giving the textfield and label room to expand it's size when needed.
Personally, if creating this particular control, I would create a UIView with a textfield and a label inside a UIStackView. That way if the label is hidden when there is no error the stackview will automatically adjust the height for you. Then when you unhide it, the view will expand to fit both controls.
A basic example:
//: Playground - noun: a place where people can play
import UIKit
import PlaygroundSupport
class LabelledTextView: UIView {
private let label = UILabel()
private let textfield = UITextField()
private let stackView = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true
stackView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
stackView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
stackView.alignment = .leading
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.addArrangedSubview(textfield)
stackView.addArrangedSubview(label)
textfield.placeholder = "Please enter some text"
label.numberOfLines = 0
label.text = "Text did not pass validation, Text did not pass validation, Text did not pass validation, Text did not pass validation"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
let labelledTextView = LabelledTextView(frame: CGRect(x: 50, y: 300, width: 300, height: 60))
let vc = UIViewController()
vc.view.addSubview(labelledTextView)
labelledTextView.translatesAutoresizingMaskIntoConstraints = false
labelledTextView.topAnchor.constraint(equalTo: vc.view.topAnchor).isActive = true
labelledTextView.leftAnchor.constraint(equalTo: vc.view.leftAnchor).isActive = true
labelledTextView.widthAnchor.constraint(equalToConstant: 300).isActive = true
labelledTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true
PlaygroundPage.current.liveView = vc.view
You need to use sizeToFit():
errorTextLabel.sizeToFit()

Resources