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

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()

Related

iOS: How to adjust uikit row height based on height of dynamic embedded swiftui view?

I'm working on a legacy app and trying to embed a swiftUI view inside a UIKit UITableView. The SwiftUI view is a LazyHGrid with a number of items based on a user's number of accounts. The idea is to display the first 1-3 items (if there are any), and a "More" button, which, when tapped, displays the rest of the items.
The SwiftUI piece is working correctly, and initially the tableView displays it correctly, like so:
three tiles working correctly. However, when I tap the "More" button, the other items pop in, but the row height in the table doesn't adjust, so they just overlap the other content in the table, like so: tiles overlapping other content.
Here's the host function that controls the swiftUI view:
func host(_ view: Content, parent: UIViewController) {
hostingController.rootView = view
hostingController.view.invalidateIntrinsicContentSize()
let requiresControllerMove = hostingController.parent != parent
if requiresControllerMove {
// remove old parent if exists
removeHostingControllerFromParent()
parent.addChild(hostingController)
}
if !contentView.subviews.contains(hostingController.view) {
contentView.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
hostingController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
hostingController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
}
if requiresControllerMove {
hostingController.didMove(toParent: parent)
}
}
I register the cell in the viewDidLoad in the UIKit controller:
self.tableView.register(SwiftUIHostTableViewCell<BillingTileGridView>.self, forCellReuseIdentifier: "BillingTileGridViewCell")
(This code was already present in the viewDidLoad):
self.tableView.estimatedRowHeight = 60
self.tableView.rowHeight = UITableView.automaticDimension
And then I create the cell like so in the tableView cellForRowAt function:
if let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseID, for: indexPath) as? SwiftUIHostTableViewCell<BillingTileGridView> {
let view = BillingTileGridView(viewModel: BillingTileGridViewModel(), adjustHeight: {
})
cell.host(view, parent: self)
return cell
}
I tried using a callback to re-load the table height for the button action, like this:
let view = BillingTileGridView(viewModel: BillingTileGridViewModel(), adjustHeight: {
tableView.beginUpdates()
tableView.endUpdates()
})
like I'd seen in questions like this one, Swift: How to reload row height in UITableViewCell without reloading data, but it didn't seem to do anything. Do I just need to calculate the height manually based on how many rows I have, and set the height explicitly to that?

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"))
}

ios - switch within navigation item bar

I want to set a UISwitch within a UINavigationBar. But when I try place my finger on the switch and drag it to "switch" on and off the view is not responding.
This is what i have.
https://github.com/rchampa/views-within-navigationItem
As already stated in the comments above your GitHub project does not contain any data. Nevertheless everything works as expected (and seems cleaner to me) if you set the custom UIBarButtonItem up programmatically:
override func viewDidLoad() {
super.viewDidLoad()
setupBarButtonItem()
}
private func setupBarButtonItem() {
let offLabel = UILabel()
offLabel.font = UIFont.boldSystemFont(ofSize: UIFont.smallSystemFontSize)
offLabel.text = "OFF"
let onLabel = UILabel()
onLabel.font = UIFont.boldSystemFont(ofSize: UIFont.smallSystemFontSize)
onLabel.text = "ON"
let toggle = UISwitch()
toggle.addTarget(self, action: #selector(toggleValueChanged(_:)), for: .valueChanged)
let stackView = UIStackView(arrangedSubviews: [offLabel, toggle, onLabel])
stackView.spacing = 8
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: stackView)
}
#objc func toggleValueChanged(_ toggle: UISwitch) {
print("new value: \(toggle.isOn)")
}
Update:
I made it work via storyboard too. In contrast to setting it up programmatically you have to embed the UIStackView into a regular UIView to be able to add it as a UIBarButtonItem in storyboard. Then I added top, leading, bottom and trailing constraints (each with a constant of 0) from the UIStackView to its superview. To get rid of the storyboard warnings and errors at design time (at runtime it works without any problems) you have to manually calculate and set the width for the outer view (which contains the UIStackView) that is needed to enclose all of it subviews (offLabel.width + spacing + toggle.width + spacing + onLabel.width).

Increase uitablview height while run time to content entire content

I created an app with Swift 3 and Xcode 8.1. I have a UITableview and a UIView above it that shows and hides by clicking on a button in it. When the UIView appears, the last cell of UITableview does not show completely.
I use following code in button:
func filterShowHide ()
{
if !isShown
{
filterImage.image = UIImage(named: "ME-Filter-re")
self.filterView.isHidden = false
self.tableViewTop.constant = 0
// tableViewHeight.constant = tableViewHeight.constant * 1.5
isShown = true
}
else
{
filterImage.image = UIImage(named: "ME-Filter")
self.tableViewTop.constant = -(self.HeaderView.frame.height) + self.filterBTN.frame.size.height
self.filterView.isHidden = true
isShown = false
// tableViewHeight.constant = tableViewHeight.constant / 1.5
}
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}
}
For more details here's the screenshot of:
Before clicking and After clicking
How can I show the last cell completely?
I think the problem is your tableview height is still the same and it gets pushed down as you update the top constraint.
You should set the table view bottom constraint to bottom of its super view or you can update tableView.contentInset.top with the values you are using for self.tableViewTop.constant

Complex Dynamic Collection Of UILabels Two Columns and Multiple Rows 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.

Resources