How to depitch stars ratings in a tableview cell? - ios

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

Related

When I add uibuttons to stack view they appear on top of each other

I'm developing a quiz app and I want to display a question with corresponding choices but only one right answer. The amount of corresponding for each question varies on the question (some have 2,3,4, etc.). The choices are displayed through UIButton's and each button is added to a UIStackView. The amount of buttons depends on how many possible choices the question has. For example, if the question has 2 choices then 2 buttons are allocated to the stackview.
However when I add a button to the stack view, each button is placed directly on top of each other but I want them to be spaced out vertically and evenly. Here is my current code:
func viewConfiguration() {
questionView.topAnchor.constraint(equalTo: headerView.bottomAnchor).isActive = true
questionView.widthAnchor.constraint(equalTo: headerView.widthAnchor).isActive = true
questionView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.385).isActive = true
choicesView.topAnchor.constraint(equalTo: questionView.bottomAnchor).isActive = true
choicesView.widthAnchor.constraint(equalTo: questionView.widthAnchor).isActive = true
choicesView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
// Adds question title label to questionVIew hierarchy
let questionLabel = UILabel()
questionLabel.translatesAutoresizingMaskIntoConstraints = false
questionView.addSubview(questionLabel)
questionLabel.backgroundColor = UIColor.yellow
questionLabel.centerXAnchor.constraint(equalTo: questionView.centerXAnchor).isActive = true
questionLabel.centerYAnchor.constraint(equalTo: questionView.centerYAnchor).isActive = true
// Adds question choices to choicesView hierarchy
var questionChoices = [UIButton]()
var buttonStack: UIStackView = {
var stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.distribution = .fillEqually
return stack
}()
let questionData = QuestionData()
questionData.presentQuestion(label: questionLabel, buttons: &questionChoices)
print("NUMBER OF BUTTONS IS --> \(questionChoices.count)")
choicesView.addSubview(buttonStack)
for button in questionChoices {
buttonStack.addArrangedSubview(button)
}
buttonStack.heightAnchor.constraint(equalTo: choicesView.heightAnchor, multiplier: 0.875).isActive = true
buttonStack.widthAnchor.constraint(equalTo: choicesView.widthAnchor, multiplier: 0.9).isActive = true
buttonStack.topAnchor.constraint(equalTo: choicesView.topAnchor, constant: 15).isActive = true
buttonStack.centerXAnchor.constraint(equalTo: choicesView.centerXAnchor).isActive = true
}
The blue section is a view that contains the stack view of buttons. The possible choices for the displayed question are "Here", "Anywhere", "Everywhere", "There", yet only the last choice is displayed. How can I make it so the choices are all viewable inside the stackview and aren't stacked on top of each other?
Screenshot of current functionality:enter image description here

Unsure how to mimic desired UI functionality

So I found the following UI pattern online and I have been attempting to implement it in Xcode. However, I have been unsuccessful. I am unsure as to whether to create the optimal approach would be to
create three different UIViewControllers (in which case I am not sure as to how to get them to animate in and out of view/how to get them to overlap one another)
or to use a UITableView with custom overlapping cells. However, I am not sure whether this approach will allow me to animate properly upon pressing each cell. For this second approach, I saw this post, but this solution does not allow for touch interaction in the overlapping areas, something which I need.
I looked for libraries online that would allow for functionality such as this, but I was unsuccessful in finding any. The animation I am trying to achieve can be found here.
I would use a StackView to hold all the views of your ViewControllers.
Then you can set the stack view's spacing property to a negative value to make the views overlap. If you wish show the complete view of one of the view controllers on tap, you add a tap gesture recognizer to that view that changes the stackView's spacing for just that view to 0.
A really simple example:
class PlaygroundViewController: UIViewController {
let firstViewController = UIViewController()
let secondViewController = UIViewController()
let thirdViewController = UIViewController()
lazy var viewControllersToAdd = [firstViewController, secondViewController, thirdViewController]
let heightOfView: CGFloat = 300
let viewOverlap: CGFloat = 200
let stackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
firstViewController.view.backgroundColor = .red
secondViewController.view.backgroundColor = .blue
thirdViewController.view.backgroundColor = .green
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = -viewOverlap
viewControllersToAdd.forEach { (controller: UIViewController) in
if let childView = controller.view {
stackView.addArrangedSubview(childView)
NSLayoutConstraint.activate([
childView.heightAnchor.constraint(equalToConstant: heightOfView),
childView.widthAnchor.constraint(equalTo: stackView.widthAnchor)
])
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapChildView(sender:)))
childView.addGestureRecognizer(gestureRecognizer)
childView.isUserInteractionEnabled = true
}
addChild(controller)
controller.didMove(toParent: self)
}
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.rightAnchor.constraint(equalTo: view.rightAnchor),
stackView.leftAnchor.constraint(equalTo: view.leftAnchor),
stackView.topAnchor.constraint(equalTo: view.topAnchor),
])
}
#objc func didTapChildView(sender: UITapGestureRecognizer) {
if let targetView = sender.view {
UIView.animate(withDuration: 0.3, animations: {
let currentSpacing = self.stackView.customSpacing(after: targetView)
if currentSpacing == 0 {
// targetView is already expanded, collapse it
self.stackView.setCustomSpacing(-self.viewOverlap, after: targetView)
} else {
// expand view
self.stackView.setCustomSpacing(0, after: targetView)
}
})
}
}
}

Collection view with autosizing cells stops working after reloading

In my app I have a collection view with cells autosizing horizontally.
Here's some code:
// called in viewDidLoad()
private func setupCollectionView() {
let cellNib = UINib(nibName: SomeCell.nibName, bundle: nil)
collectionView.register(cellNib, forCellWithReuseIdentifier: SomeCell.reuseIdentifier)
guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
flowLayout.itemSize = UICollectionViewFlowLayout.automaticSize
}
The cell has 1 view, which has constraint for heigth. This view subviews a label, which is limited with 2 rows and is not limited by width. The idea here is to allow label to calculate its own width fitting text in 2 rows.
In order to make this work I've added the following code to the cell class:
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
contentView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
Now, it works perfectly. Cells are autosizng, scrollView is scrolling horizontally etc. Until I call reloadData() at least once. Then cells have size 50x50 and never autosize any more until I leave the screen and come back again.
Do you have any ideas on why is it happening?

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

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