Complex Dynamic Collection Of UILabels Two Columns and Multiple Rows iOS - ios

I've been struggling thinking how to setup this kind of layout in my tableViewCell. See photo:
More info:
Data is dynamic. There might be other days and each days might consists of multiple set of time.
Auto-layout is needed of course for dynamic/responsive height of my cell.
What I already did:
I did try doing this using IB.
I also tried it programmatically.
Both in IB and programmatically, I used UIStackView. But kinda hard to set it up.
I'm thinking to set it up using UIViews as containers, just like UIStackView but less complex.
I'm setting this up row by row. First is to line up the time vertically, and then the view or stackview of that will be paired horizontally with the Day. After that, do the same with the other days.
For formality of the question, here is a part of my code in my cell for setting up this layout, I suggest not taking an effort to read it, I believe I know what I am doing, and I think I just need another approach from you guys.
var job: Job! {
didSet {
_ = self.subviews.map {
if $0 is UIStackView {
$0.removeFromSuperview()
}
}
GPLog(classSender: self, log: "🎉Setting up stackview for JobShift")
// Add the main vertical stackView
let stackView_Vertical = UIStackView(frame: .zero)
stackView_Vertical.translatesAutoresizingMaskIntoConstraints = false
stackView_Vertical.alignment = .fill
stackView_Vertical.distribution = .fillProportionally
stackView_Vertical.axis = .vertical
stackView_Vertical.spacing = 16.0
self.addSubview(stackView_Vertical)
// Add constraints
stackView_Vertical.topAnchor.constraint(equalTo: self.topAnchor, constant: 15.0).isActive = true
stackView_Vertical.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -15.0).isActive = true
stackView_Vertical.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15.0).isActive = true
stackView_Vertical.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15.0).isActive = true
if let dummyJson = self.readJson() {
if let shiftsJsonArray = dummyJson.array {
for shiftJson in shiftsJsonArray {
let newShift = DummyDataShift(json: shiftJson)
if let day = newShift.day,
let schedules = newShift.schedule {
let generatedStackView = self.generateDayScheduleStackView(day: day, schedules: schedules)
stackView_Vertical.addArrangedSubview(generatedStackView)
}
}
}
}
}
}
// MARK: - Functions
// Generate the full schedule stack view.
func generateDayScheduleStackView(day: String, schedules: [DummyDataSchedule]) -> UIStackView {
// label day (e.g. MONDAY)
let newLabel_Day = self.shiftLabel
newLabel_Day.translatesAutoresizingMaskIntoConstraints = false
newLabel_Day.text = day
newLabel_Day.backgroundColor = .red
newLabel_Day.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
// Prepare the vertical schedule stackView
let stackView_Schedule = UIStackView(frame: .zero)
stackView_Schedule.alignment = .fill
stackView_Schedule.distribution = .fillEqually
stackView_Schedule.axis = .vertical
// Add the schedules to the stackView vertically
for schedule in schedules {
let newLabel_Time = self.shiftLabel
newLabel_Time.text = "\(schedule.timeIn!) - \(schedule.timeOut!)"
newLabel_Time.backgroundColor = self.getRandomColor()
newLabel_Time.translatesAutoresizingMaskIntoConstraints = false
newLabel_Time.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
stackView_Schedule.addArrangedSubview(newLabel_Time)
}
// Prepare the horizontal dayScheduleStackView
let stackView_DaySchedule = UIStackView(frame: .zero)
stackView_DaySchedule.alignment = .fill
stackView_DaySchedule.distribution = .fillProportionally
stackView_DaySchedule.axis = .horizontal
// Add arranged subViews
stackView_DaySchedule.addArrangedSubview(newLabel_Day)
stackView_DaySchedule.addArrangedSubview(stackView_DaySchedule)
return stackView_DaySchedule
}
Problem is: Lots of warnings for broken constraints, I do know how to set up and fix constraints. But when I fix it, nothing is displaying. I feel like I'm wasting my time pushing and trying hard to continue this approach. So I thought that it would be me a lot if I ask for suggestions?

There is multiple ways of solving your problem. I will try to give you one approach to it.
1) Use a TableView, with each section cell containing the "day of the week" label plus a vertical StackView for the time labels (use equal spacing).
2) If you set your constraints properly, you can return UITableViewAutomaticDimension on sizeForRow: delegate.
override func tableView(_ tableView: UITableView, heightForRowAt
indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
Then, on the cellForRowAt method, you can append labels to your vertical stack view with the times. The height of each cell will be correct as it will come from the constraints.

Actually, I think stack views a perfectly good way to go. They are about arranging things in rows and columns, which is precisely what you want to do. I had no difficulty arranging some labels in imitation of your specifications, using stack views alone, in Interface Builder:
The delightful thing about stack views is that once they are configured properly, they adapt as arranged subviews are added or removed. So they are dynamic in exactly the way you desire.

Related

UITableView setting row Height not having effect

Why is it that when I set tableView.rowHeight = 100 in viewDidLoad() I always get the default height value of 44.0? I tried setting tableView.estimatedHeight =100 as well but no luck, I tried setting the delegate method tableView.heightForRowAt as well but that doesn't seem to have any effect as well what so ever. So the question is: how do you set the height for a tableView Cell?
override func viewDidLoad(){ // tableView viewDidLoad
super.viewDidLoad()
tableView.rowHeight = 100
tableView.register(TickerCell.self, forCellReuseIdentifier: "Cell")
}
// Custom Cell Init
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .clear
print(frame.height) // always prints 44.0
let symbolstack = UIStackView(arrangedSubviews: [symbolLabel,companyLabel])
let sectorstack = UIStackView(arrangedSubviews: [sectorLabel,exchangeLabel])
let mainStack = UIStackView(arrangedSubviews: [symbolstack,sectorstack])
sectorstack.axis = .vertical
symbolstack.axis = .vertical
sectorstack.alignment = .trailing
symbolstack.alignment = .leading
symbolstack.distribution = .fillEqually
sectorstack.distribution = .fillEqually
mainStack.distribution = .fillProportionally
addSubview(mainStack)
mainStack.translatesAutoresizingMaskIntoConstraints = false
mainStack.heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
mainStack.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -45).isActive = true
mainStack.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15).isActive = true
addSubview(WatchlistStar)
WatchlistStar.translatesAutoresizingMaskIntoConstraints = false
WatchlistStar.widthAnchor.constraint(equalToConstant: 15).isActive = true
WatchlistStar.leadingAnchor.constraint(equalTo: mainStack.trailingAnchor, constant: 20).isActive = true
WatchlistStar.heightAnchor.constraint(equalToConstant: 15).isActive = true
WatchlistStar.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
}
So the problem appears to be in the fact then in the cell init method the rowHeight I set In viewDidLoad does not seem to register, only in prepareForReuse when I print frame.height do I get 100 printed,So in which method do I setup the cell layout then?
A cell is not born knowing its height, so init is a pointless place to look at the frame. In fact, it has no inherent height. Cells are reused. They take on a height only in relation to a particular row of the table where they are being used right at the moment. That height can change when the cell is used in a different row (because rows can be different heights). So your layout needs to cope with that.
In the code you have shown, you are using autolayout. The whole point of autolayout is that you don't care about the frame of things at any one moment. Everything adjusts automatically as the surrounding frame changes. Autolayout is about relationships.
So the solution in that case is: don't look at frame.height. You don't need to know it. Just lay out the relationships between the views and the cell, and everything will be correct when the cell appears, if you have used autolayout correctly.
On the other hand, as you now say in a comment "Im forced to [use autolayout] not because I want to" — okay, so if the goal is to do layout manually, like we did before there was autolayout, then the place to do it is in the data source's cellForRowAt:. Or you could try doing it in the cell's layoutSubviews if you want the cell to lay itself out. See the old edition of my book, online, for how we used to do this: http://www.apeth.com/iOSBook/ch21.html#_custom_cells
Be very careful to distinguish between adding subviews and resizing them. You don't want to make the mistake of adding subviews that you have already added. So add the subviews in init, sure, as it is called only once, but size them in a place where the actual size of the row has been communicated to the cell.
One more piece of advice. Your current code uses the phrase addSubview, meaning self.addSubview. That is totally wrong and illegal. Never never add a subview directly to a cell. Add it only to the cell's contentView, and size it in relation to that.
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.rowHeight = UITableView.automaticDimension;
self.tableView.estimatedRowHeight = 40
}

How to depitch stars ratings in a tableview cell?

I am fetching totalRaters and totalRatings from the back-end. I divide the latter by the former to determine how many stars I should show. It'll always be a number between 0 and 5, inclusive.
My star ratings code in the UITableViewCell subclass is:
fileprivate let starStack : UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.distribution = .fillEqually
stackView.spacing = 4
return stackView
}()
func setupStar() {
//... code to add a label
backgroundView.addSubview(starStack)
starStack.translatesAutoresizingMaskIntoConstraints = false
starStack.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 8).isActive = true
starStack.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: 8).isActive = true
starStack.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: -8).isActive = true
starStack.heightAnchor.constraint(equalToConstant: 34).isActive = true
}
func setValues(totalRatings Int, totalRaters Int) {
let ratings = totalRatings / totalRaters
if ratings > 0 {
for index in 0...ratings - 1 {
arrayStar[index].image = UIImage(named: "icon_starred")
}
}
}
The problem is that whenever I scroll down (i.e. the cell disappears beneath the view port) and then back up, the stars keep adding up until all 5 stars get populated. This happens for all of the table view cells. I am not sure what I am doing wrong. I added images to indicate the problem below. (They got uploaded in the reverse order)
Table view cells may get reused as you scroll up and down the table. You are setting images in the array but never clearing them so when the cell is reused it will retain whatever was there for its previous use.
You should add a prepareForReuse method to you table view cell class and clear the array of images. Something like:
override func prepareForReuse() {
super.prepareForReuse()
arrayStar = [UIImage](count: 5, repeatedValue: UIImage(named: "icon_not_starred"))
}

UITableViewCell doesn't change height when some UIStackView's subviews are unhidded

As the title says, I have a custom UITableCell in which I have some UIStackViews. Each of those stacks contains many subviews but I just want to show three of them when the cell is displayed for the first time. If a user wants to see more, there is a [+] button that calls a method that adds the remaining.
The custom cell height is determined via UITableViewAutomaticDimension and it works perfectly for the first display of the cell but when I try to add and remove subviews to the stack, there are views that shouldn't be modified that lose they constraints and the ones that should be displayed doesn't do it in some cases. What I'd like is to show all the UILabels and the height of the cell to be updated.
The method that is called when the button [+] is pressed is the following:
#objc private func changeImage(sender: UIButton) {
let index = (Int(sender.accessibilityValue!)!)
let open : Bool = openItem[index]
let plateStack : UIStackView = plateStacks[index]
let plates : [UILabel] = platesViews[index]
if !open {
sender.setImage(UIImage(named: "less")?.withRenderingMode(.alwaysTemplate), for: .normal)
let nPlatesToAdd = max(platesViews[index].count - 3, 0)
for i in 0..<nPlatesToAdd {
let plate = plates[i + 3]
plateStack.addArrangedSubview(plate)
plate.leadingAnchor.constraint(equalTo: plateStack.leadingAnchor, constant: 0).isActive = true
plate.trailingAnchor.constraint(equalTo: plateStack.trailingAnchor, constant: 0).isActive = true
}
}
else {
sender.setImage(UIImage(named: "more")?.withRenderingMode(.alwaysTemplate), for: .normal)
var i = plateStack.arrangedSubviews.count - 1
while i > 2 {
let view = plateStack.arrangedSubviews[i]
plateStack.removeArrangedSubview(view)
view.removeFromSuperview()
i = i - 1
}
}
openItem[index] = !open
}
The first display of the cell (everything's ok) and after click on the [+] button:
It happened because tableView is already rendered its layout.
You might need to check some causes :
make sure the stackView constraint is properly put to contentView
stackView's distribution must be fill
After you change something that affects tableView height, you can use these code to update cell height without reloading the table:
tableView.beginUpdates()
tableView.endUpdates()

UITableview stops responding when using translatesAutoresizingMaskIntoConstraints=false

I want to add a view with a tableview in it in code.
I define the view in storyboard like in the image.
In my view controller I set the place and size using the following code.
override func viewDidLoad() {
super.viewDidLoad()
// bunch of non-related code
timeTableView.delegate = self
timeTableView.dataSource = self
timeTableView.rowHeight = UITableViewAutomaticDimension
timeTableView.estimatedRowHeight = 32
timeChooser.translatesAutoresizingMaskIntoConstraints=false
}
#IBAction func onTimeLabelPressed(_ sender: AnyObject){
view.addSubview(timeChooser)
let topConstraint = timeChooser.topAnchor.constraint(equalTo: viewdayTime.bottomAnchor)
let leftConstraint = timeChooser.leftAnchor.constraint(equalTo: viewdayTime.leftAnchor)
//let heightConstraint = timeChooser.heightAnchor.constraint(greaterThanOrEqualToConstant: 128)
//let widthConstraint = timeChooser.widthAnchor.constraint(lessThanOrEqualTo: viewdayTime.widthAnchor)
NSLayoutConstraint.activate([topConstraint,leftConstraint, /*heightConstraint, widthconstraint*/])
view.layoutIfNeeded()
timeChooser.alpha=0
UIView.animate(withDuration: 0.2){
self.timeChooser.alpha=1.0}
}
The function onTimeLabelPressed makes the view appear under and left aligned with a label.
Problem is the tableview will not scroll or respond otherwise as long as the line
timeChooser.translatesAutoresizingMaskIntoConstraints=false
is present. Removing it breaks the positioning constraints but the tableview behaves normally.
Anybody any ideas? Did not find any question describing my witnessed behavior.
You need to give your timeChooser constraints to establish its width and height. It would probably be best to set right and bottom constraints.
let rightConstraint = timeChooser.rightAnchor.constraint(equalTo: view.rightAnchor)
let bottomConstraint = timeChooser.bottomAnchor.constraint(equalTo: view.bottomAnchor)

UIStackView children equally spaced (around and between)

How can I have an UIStackView with the same space as padding and gap between views?
How can I achieve this layout:
When this one doesn't suit me:
Neither does this:
I just need the around views space to be the same as the between views space.
Why is it so hard?
Important
I'm using my fork of TZStackView to support iOS 7. So no layoutMargins for me :(
I know this is an older question, but the way I solved it was to add two UIViews with zero size at the beginning and end of my stack, then use the .equalSpacing distribution.
Note: this only guarantees equal around spacing along the main axis of the stack view (i.e. the left and right edges in my example)
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.distribution = .equalSpacing
// add content normally
// ...
// add extra views for spacing
stack.insertArrangedSubview(UIView(), at: 0)
stack.addArrangedSubview(UIView())
You can almost achieve what you want using a UIStackView. When you set some constraints yourself on the UIViews inside the UIStackView you can come up with this:
This is missing the left and right padding that you are looking for. The problem is that UIStackView is adding its own constraints when you add views to it. In this case you can add top and bottom constraints to get the vertical padding, but when you try to add a trailing constraint for the right padding, UIStackView ignores or overrides that constraint. Interestingly adding a leading constraint for the left padding works.
But setting constraints on UIStackView's arranged subviews is not what you want to do anyway. The whole point of using a UIStackView is to just give it some views and let UIStackView handle the rest.
To achieve what you are trying to do is actually not too hard. Here is an example of a UIViewController that contains a custom stack view that can handle padding on all sides (I used SnapKit for the constraints):
import UIKit
import SnapKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let padding: CGFloat = 30
let customStackView = UIView()
customStackView.backgroundColor = UIColor(white: 0, alpha: 0.1)
view.addSubview(customStackView)
customStackView.snp_makeConstraints { (make) -> Void in
make.top.left.equalTo(padding)
make.right.equalTo(-padding)
}
// define an array of subviews
let views = [UIView(), UIView(), UIView()]
// UIView does not have an intrinsic contentSize
// so you have to set some heights
// In a real implementation the height will be determined
// by the views' content, but for this example
// you have to set the height programmatically
views[0].snp_makeConstraints { (make) -> Void in
make.height.equalTo(150)
}
views[1].snp_makeConstraints { (make) -> Void in
make.height.equalTo(120)
}
views[2].snp_makeConstraints { (make) -> Void in
make.height.equalTo(130)
}
// Iterate through the views and set the constraints
var leftHandView: UIView? = nil
for view in views {
customStackView.addSubview(view)
view.backgroundColor = UIColor(white: 0, alpha: 0.15)
view.snp_makeConstraints(closure: { (make) -> Void in
make.top.equalTo(padding)
make.bottom.lessThanOrEqualTo(-padding)
if let leftHandView = leftHandView {
make.left.equalTo(leftHandView.snp_right).offset(padding)
make.width.equalTo(leftHandView)
} else {
make.left.equalTo(padding)
}
leftHandView = view
})
}
if let lastView = views.last {
lastView.snp_makeConstraints(closure: { (make) -> Void in
make.right.equalTo(-padding)
})
}
}
}
This produces the following results:
For those who keep getting here looking for a solution for this problem. I found that the best way (in my case) would be to use a parent UIView as background and padding, like this:
In this case the UIStackView is contrained to the edges of the UIView with a padding and separate the subviews with spacing.

Resources