Table cell only updates when I select it - ios

I'm making an iOS app which uses a table, and each cell contains two labels and a custom view I made called ColourCircle. ColourCircle has a property called fill: UIColor and I've overridden the draw(_ rect: CGRect) function to draw a circle of that colour.
The cells' data are loaded from an array: data: [(String, Double)], using the view controller as both the UITableViewDelegate and UITableViewDataSource.
In viewWillAppear I've written:
guard let fetchedData = getArtistData() else {
performSegue(withIdentifier: "data-error", sender: nil)
return
}
chart.load(data: fetchedData)
data = fetchedData
table.reloadData()
table.setNeedsLayout()
table.isHidden = false
chart.isHidden = false
activityIndicator.stopAnimating()
And, in tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) I've written:
let cell = table.dequeueReusableCell(withIdentifier: "artist cell") as! ArtistCell
let name = data[indexPath.row].0
if let proportion = chart.getPercentage(of: name) {
let percentage = proportion * 100
cell.name.text = name
cell.percentage.text = "\(round(percentage * 10) / 10)%" // rounded to 2 d.p.
cell.colour.fill = chart.colour(for: name)!
}
return cell
When I run my app, the visible cells (i.e. which are on the screen) work perfectly, and the ColourCircle views display the correct colour. When I scroll down, bringing new cells into view, the new cells don't have the correct colour, but instead use the colours from the previously loaded cells, in order.
For example, say the first two cells in the table are red and blue, then the first two which load when I scroll down will also be red and blue, in that order. The same thing happens when I scroll up, re-loading the previous cells.
When I select these cells, however, they change to the correct colour. Then, when unloaded and loaded again, they will stay the correct colour unless I've fixed one of the cells further up (by selecting them), in which case they will change to the colours of the ones I fixed.
It seems like it's something to do with reusing the cells, although I can't quite work out how to fix it.
EDIT: Here's the code for my ArtistCell class:
class ArtistCell: UITableViewCell {
#IBOutlet weak var colour: ColourCircle!
#IBOutlet weak var name: UILabel!
#IBOutlet weak var percentage: UILabel!
}

according this: let cell = table.dequeueReusableCell(withIdentifier: "artist cell") as! ArtistCell - all cells are reusable. Use prepareForReuse() method, to prepare cell to reuse and clean up they content.

Related

How to draw a circle around UICollectionViewCell when clicking on it and remove others circle from previous selected cells?

I've been working with UICollectionView lately. There is a requirement that needs to be implemented like: "There are several imageviews in several collectionview cell. When user selects one of the image/cell, the app will draw a blue circle around that image/cell."
Currently, I'm able to do the draw on the cell. But the problem now is that I am able only to draw all cells but not one cell at the time (as screenshot below)
So my question is: how can I select one image/cell, the blue circle of previous selected cell should be removed?
Thanks so much for the answers in advance.
It sounds like you want this:
You didn't say how you're putting the blue circle in the cell. Here's how I think you should handle selection: use the collection view's built-in selection support as much as possible.
A UICollectionView already has support for selecting cells. By default, its allowsSelection property is true and its allowsMultipleSelection property is false, so it allows the user to select one item at a time by tapping the item. This sounds like almost exactly what you want.
The collection view makes the current selection available in its indexPathsForSelectedItems property, which is either nil or empty when no cell is selected, and contains exactly one index path when one item is selected.
When an item is selected, and there is a visible cell for the item, the cell shows that its item is selected by making its selectedBackgroundView visible. So make a UIView subclass that shows a blue circle:
class CircleView: UIView {
override class var layerClass: AnyClass { return CAShapeLayer.self }
override func layoutSubviews() {
super.layoutSubviews()
let layer = self.layer as! CAShapeLayer
layer.strokeColor = UIColor.blue.cgColor
layer.fillColor = nil
let width: CGFloat = 3
layer.lineWidth = width
layer.path = CGPath(ellipseIn: bounds.insetBy(dx: width / 2, dy: width / 2), transform: nil)
}
}
Then use an instance of CircleView as the cell's selectedBackgroundView. You can create the instance lazily the first time the cell becomes selected:
class MyCell: UICollectionViewCell {
override var isSelected: Bool {
willSet {
if newValue && selectedBackgroundView == nil {
selectedBackgroundView = CircleView()
}
}
}
var title: String = "???" {
didSet {
label.text = title
}
}
#IBOutlet private var label: UILabel!
}
With this code in place, the user can tap a cell to select its item, and the cell will show a blue circle when selected. Tapping another cell will deselect the previously-selected item, and the blue circle will “move” to the newly-selected item's cell.
You might want to let the user deselect the selected item by tapping it again. UICollectionView doesn't do that by default if allowsMultipleSelection is false. One way to enable tap-again-to-deselect is by implementing collectionView(_:shouldSelectItemAt:) in your UICollectionViewDelegate:
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
if (collectionView.indexPathsForSelectedItems ?? []).contains(indexPath) {
// Item is already selected, so deselect it.
collectionView.deselectItem(at: indexPath, animated: false)
return false
} else {
return true
}
}

Swift UITableViewCell fails to setup its constraint after cellForRowAtIndexPath

In my tableViewController i use a custom cell which contains an imageView, two labels and some other irrelevant elements. Also there's a constraint, of which the constant value shall be changed, if a certain condition is given. This works fine on the first sight but if i scroll down and get cells where the constraint constant is set to another value the some of the following cells kinda keep this constant from this previous cell and don't set it up at appearing.
This is the relevant part of my cellForRowAtIndexpath:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = whichCellToShow ? "ThisCell" : "TheOtherCellCell"
//...
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
if let customCell = cell as? CustomCellProtocol,
indexPath.section < data.count,
indexPath.row < data[indexPath.section].count {
customCell.display(data: data[indexPath.section][indexPath.row])
}
return cell
}
These are the relevant parts of my custom cell class:
class CustomCell: UITableViewCell, CustomCellProtocol {
#IBOutlet weak var picture: UIImageView!
#IBOutlet weak var title: UILabel!
#IBOutlet weak var weblink: UILabel!
#IBOutlet weak var titleIndentationConstraint: NSLayoutConstraint!
//...
func display(data: Data) {
//...
title.text = data.title
//...
weblink.text = data.hasWeblink ? URLUtilities.hostOfURL(urlstr: data.sourceUrl) : nil
weblink.isHidden = !data.hasWeblink
//This is the spot where things seem to go wrong
titleIndentationConstraint.constant = data.hasWeblink ? weblink.frame.height : 0
setNeedsLayout()
layoutIfNeeded()
}
}
If data.hasWeblink == true the constraint constant shall be equal to the height of the weblink label, if it's false it shall be 0.
My first thought was, that the view recycling of UITableView could collide with titleIndentationConstraint.constant = data.hasWeblink ? weblink.frame.height : 0. That's why i called setNeedsLayout()and layoutIfNeeded()straight afterwards, but this doesn't seem to help. All the other elements of the cell do their job correctly.
Also i'm setting tableView.rowHeightand tableView.estimatedRowHeightinside the viewDidLoad() of my tableViewController.
Does anybody have an idea what in particular is going wrong there or what i forgot to do? Thanks in forward!
Fixed it myself, had to change some constraint priorities of the title label:
My title label has a bottom constraint which is always greater or equal than 8 pts. Also i limited the number of lines to 4 and told the label to truncate the text's tail, if it's too long to be displayed. Funnily always, if the text was truncated, i got this silly behavior described above, so it has absolutely nothing to do with my titleIndetantionConstraint. Seems like if the text has to be truncated the label aligned itself to the bottom constraint and kinda dragged the content to center left (so watch what content mode says in the property editor, it should be left by default). While changing the label's content mode seems like a bad idea to me, i lowered the priority of the title label bottom constraint by 1 so it aligns itself to the top of the picture, like i planned. Hopefully this helps people how have similar issues B).

Use UICollectionViews to create dynamic and multiple features

I'm interested in creating a view that contains multiple features that users can scroll down and see i.e. pictures, description, comments, carousels etc. I am aware that the UICollectionView is able to provide this type of layout. I initially thought UITableViews would be the best approach.
I have looked at several tutorials and GitHub repos but majority of them just use a UICollectionView in a standard grid layout. I've also looked at IGListKit used by Instagram and some the tutorials linked to it.
I'm aiming to get something like the KitchenStories app:
I was wondering if someone could advice me in terms of the direction and approach best for this.
Don't try to do too much with any single view, even a UICollectionView.
The screen you've shown has a UITabBarController manage its top-level arrangement. The currently selected tab (“Home”) has a UINavigationController managing its content.
On the top of the navigation stack is, probably, a collection view or a table view. Either could be used here because the elements are visually laid out as screen-width rows in a stack. A table view is simpler because then you don't have to worry about setting up the layout.
The table view has several visible rows, each different:
The title/image row (“Easy seafood paella”)
The ratings row
The export row (hearts / save / share)
The comments row
The creator row (I assume, since it looks like it's probably a headshot and a name)
And there are probably even more unique rows out of view.
In your storyboard, you can design each of these rows as a prototype row in the table view controller's scene. Or you can design the table view with static content rows, which is easier if you won't need to change the order of the rows or duplicate any rows at run time.
“But Rob,” you say, “I can't fit all those rows into the table view in my storyboard!” Make the storyboard scene taller. UIKit will resize it at run time to fit the device screen.
In each row, drag in and arrange whatever subviews you need for that row's data. For example, the title/image row needs a UIImageView and a UILabel. The ratings row needs a label, probably a custom view to display and edit the stars, and perhaps a stack view for layout.
For each row, you'll need a separate subclass of UITableViewCell with outlets to that row's views. To pass the data to each cell for display, make each cell conform to a protocol:
protocol RecipeUsing {
var recipe: Recipe? { get set }
}
Then, in your table view controller, you set it like this if you're using static content:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = super.tableView(tableView, cellForRowAt: indexPath)
if let user = cell as? RecipeUsing {
user.recipe = recipe
}
return cell
}
You'll need a RecipeTitleImageCell with outlets to the UIImageView and UILabel. Something like this:
class RecipeTitleImageCell: UITableViewCell, RecipeUsing {
#IBOutlet var label: UILabel!
// UITableViewCell has an imageView property that's not an outlet 😭
#IBOutlet var myImageView: UIImageView!
var recipe: Recipe? {
didSet {
guard let recipe = recipe else { return }
label.text = recipe.title
myImageView.image = recipe.image
}
}
}
And for the ratings row, you'll want something like this:
class RecipeRatingsCell: UITableViewCell, RecipeUsing {
#IBOutlet var ratingControl: RatingControl!
#IBOutlet var label: UILabel!
var recipe: Recipe? {
didSet {
guard let recipe = recipe else { return }
ratingControl.rating = recipe.ratings.reduce(0, +) / recipe.Double(ratings.count)
if ratings.count < 5 { label.text = "Too few ratings" }
else { label.text = "\(ratings.count) ratings" }
}
}
}

Make UIButton stick on bottom of UITableView

I have an UITableView which consists of prototype cells. I want to put an UIButton inside the bottom of the UITableView using Interface Builder.
I added the UIButton in the footer of the UITableView:
I added a purple background for the Footer View and a green background colour for the UITableView. In the picture above it shows the Button at the bottom of the footer. However this isn't equal to the bottom of the UITableView.
The GIF below displays that the button is placed bellow the cells but not inside the bottom of the UITableView. I want it to appear at the bottom in the UITableView. Not under the UITableView. The following GIF displays this problem:
My question is: How do I set an UIButton inside an UITableView at the bottom of the UITableView using Interface Builder?
This is what I want to achieve (From Apple's ResearchKit):
Edit: The UIButton should be inside the UITableView. Suggestions where the UIButton is placed outside the TableView and pinned underneath don't achieve my goal.
You are setting footer width wrong.Set it fixed height so that button sticks to that particular height(Should be Fixed like 60px)
Check Demo Code for Storyboard structure and constraints
So I had to slightly swizzle it, but got it working by doing the below things:
Pull the UIButton out to the same level in the view heirarcy as
the tableview.
Embed the tableview and the button inside a view
Embed the above view inside another view
Pin edges of view #3 (Pinned View) to superview
Pin top, left & right edges of view #2 (Resizing View) to view #3 edges. And set a constraint of equal height to view #3.
Set an outlet in the view controller for the equal height constraint
The view heirarcy in IB should look like this:
Now in the view controller code, you need to do the following things:
Create instance var for the keyboard offset value
var keyboardOffset: CGFloat = 0
set notifications and observers for the keyboard willShow and
willHide
notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(_:)), name:NSNotification.Name.UIKeyboardWillShow, object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardWillHide(_:)), name:NSNotification.Name.UIKeyboardWillHide, object: nil)
In keyboardWillShow, cache the keyboard height value.
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
keyboardOffset = keyboardSize.height
}
Create didSet method on the keyboardOffset var, and animate the height of the view by that value each time it is set
var keyboardOffset: CGFloat = 0 {
didSet {
resizingViewHeight.constant = -keyboardOffset
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}
}
}
Make sure you set the offset back to 0 in keyboardWillHide
keyboardOffset = 0
Every time the keyboard now appears, the view that is containing the tableview will reduce in size and therefore pull the contents up with it, providing the shrinking tableview effect that you are hoepfully looking for!
Add a view that contains the UIButton to the bottom of the UIViewController where the UITableView is. Give it the constraints to attach to left, right and bottom side of super view and probably a fixed height.
Then attach the UITableView's bottom constraint to the top of the view that contains the UIButton.
You should get the effect you're looking for.
NOTE: For the button you can give centered Y and X in superview constraints to keep it centered.
Footer is apperead always after the last cell of your table view so your output is correct.
If you wanted the button bottom of tableview then add button below the tableview in hierarchy not as a footer. But it makes your button static that means it didn't matter how much cells you have, button is always button of the tableView but it is not a scrollable like as it is now.
I tried the accepted answer, but couldn't get it to work. I found that the footer view always stayed pinned to the bottom of the screen, regardless of the size of the TableView (just as if it were a sibling of the TableView). I ended up following an approach suggested here: https://stackoverflow.com/a/18047772/5778751 The basic idea is that you programmatically determine the height of the TableView and depending on the result, you EITHER display a footer internal to the TableView OR display a view which is a sibling of the TableView.
I have a perfect solution for this problem. Using default was never that meaningful in my life.
The button under the view is also a table view cell from another section but its configuration of header height and interior design is just different from the above cells.
So I have five different sections. The first three of them are standard table view cells(SettingTableViewCell) but the last two(cache and version) are custom buttons. In the header title, I init for those empty titles.
enum Section: Int {
case adjustSettings
case about
case agreements
case cache
case version
static var numberOfSections: Int { return 5 }
var reuseIdentifier: String { return "SettingTableCell" }
var headerTitle: String? {
switch self {
case .adjustSettings: return "settings.adjust.section.title".localized
case .about: return "settings.headertitle.about".localized
case .agreements: return "agreement.title".localized
case .cache: return ""
case .version: return ""
}
}
Then I configured with cell will be in which section with below code. Cache and version have only one cell which will be our buttons.
var cells: [CellType] {
switch self {
case .adjustSettings:return [.notification,.language ]
case .about: return [.rate, .contact, .invite]
case .agreements: return [.membership, .kvkk, .illuminate]
case .cache: return [.cache]
case .version: return [.version]
}
}
I have three different set functions inside my settingsTableViewCell.
For setting up standard table view cell -> .setDefault(text: text)
For setting up my clean cache button -> .setCache(text: text)
Last for shoving version info -> .setVersion(version: version)
with the above cellForRowAt, I am switching rows and setting them up accordingly. My default is .setDefault
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let section = Section(rawValue: indexPath.section) else {
assertionFailure()
return UITableViewCell()
}
let row = section.cells[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: section.reuseIdentifier) as! SettingTableCell
switch row {
case .version:
cell.setVersion(version: getVersion())
case .cache:
ImageCache.default.calculateDiskCacheSize(completion: { size in
if size == 0 {
cell.setCache(text: "settings.clear.data".localized)
} else {
let byte = Int64(size)
let fileSizeWithUnit = ByteCountFormatter.string(fromByteCount: byte, countStyle: .file)
cell.setCache(text: "settings.cler.data.with.string".localized + "(\(String(describing: fileSizeWithUnit)))")
}
})
default:
cell.setDefault(text: row.text)
}
return cell
}
You can adjust button heights as below by switching section.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let section = Section(rawValue: indexPath.section) else { return 0 }
switch section {
case .cache: return 44
case .version: return 44
default: return 56.0
}
You can adjust the gap between each button as below.
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard let section = Section(rawValue: section) else { return 0 }
switch section {
case .adjustSettings: return 46
case .about: return 46
case .agreements: return 46
case .cache: return 9
case .version: return 0.5
default: return 46
}
And finally, this is my cell where I set .set functions to customize each cell as I pleased.
class SettingTableCell: UITableViewCell {
#IBOutlet weak var line: UIView!
#IBOutlet weak var content: UIView!
#IBOutlet weak var arrowView: UIView!
#IBOutlet weak var labelSetting: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
func setVersion(version: String) {
arrowView.isHidden = true
line.isHidden = true
content.backgroundColor = .clear
labelSetting.label(textStr: version, textColor: KSColor.neutral400.getColor(), textFont: .sfProTextRegular(size: 13), fontSize: 13, lineSpacing: -0.13, paragraphStyle: NSMutableParagraphStyle())
labelSetting.textAlignment = .center
self.accessoryType = .none
}
func setCache(text: String) {
arrowView.isHidden = true
line.isHidden = true
content.backgroundColor = KSColor.neutral100.getColor()
labelSetting.label(textStr: text, textColor: KSColor.neutral700.getColor(), textFont: .sfProTextMedium(size: 14), fontSize: 14, lineSpacing: -0.14, paragraphStyle: NSMutableParagraphStyle())
labelSetting.textAlignment = .center
self.accessoryType = .none
}
func setDefault(text: String) {
labelSetting.label(textStr: text, textColor: KSColor.neutral700.getColor(), textFont: UIFont.sfProTextMedium(size: 16), fontSize: 16, lineSpacing: -0.16, paragraphStyle: NSMutableParagraphStyle())
}
}
And the outcome is I have 5 sections but the last two are buttons.

UIScrollView position in reusable cell affecting UIScrollView position in other cell

I'm building an iOS app with Swift where a user can scroll through a feed of images up and down (like instagram) and can also scroll left or right on the cell to see more images. Refer to this album(not able to upload images yet) and see F1 for how the UI is laid out.
My problem is when I scroll over to image 1C, the 3rd cell is also scrolling over to 3C. So when I scroll down to the third cell, it's already at 3C. See F2.
Additionally, if I scroll on the 3rd cell to 3B, it also repositions the first cell to 1B. See F3.
I'd like some help in understanding what's going on...
I've created a custom class for the cell with a function to load the images.
(Where I also initialize the imageView frames, scrollView frame and content size, and other properties which I'm not including in the code below.)
class CustomCell: UITableViewCell {
var imageView1: UIImageView = UIImageView()
var imageView2: UIImageView = UIImageView()
var imageView3: UIImageView = UIImageView()
func loadImages(#image1: String, image2: String, image3: String){
imageView1.image = UIImage(named: image1)
imageView2.image = UIImage(named: image2)
imageView3.image = UIImage(named: image3)
}}
and in my View Controller - cellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:CustomCell = self.tableView.dequeueReusableCellWithIdentifier("CustomCell") as CustomCell
var (firstImage, secondImage, thirdImage) = images[indexPath.row]
cell.loadImages(image1: firstImage, image2: secondImage, image3: thirdImage)
return cell
}
FYI: This appears to only be happening when the view is the root view controller. It works fine when the view has been presented. ???
Thanks!
The UITableView dequeues reusable cells. That means that your horizontal scrollview in your cell will have the same content offset as the cell he dequeued. So if you scroll to let's say contentOffset.x = 100.0 in cell 1 and than scroll down in your tableview, it might happen that one of the following cells will have its scrollview subview with the same contentOffset.
Just set contentOffset.y = 0 in your cellForRowAtIndexPath.
Or better: Save the cell's selected imageIndex and set an appropriate contentOffset.
Reuseable means exactly that: to speed things up the cells gets reused. But as you are changing view properties these are present when the cell is populated with new values. Set the values back on population in cellF orRiwAtIndexPath or – cleaner IMHO – in cellWillDisplay.

Resources