Why UITableViewCell is not accessible (for VoiceOver) - ios

I'm not trying to solve any problem. Of course you can set isAccessibilityEnabled = true and it works. My question is: why is it turned off by default and there is no appropriate section in the interface builder. It seems for me like it's not recommended to make UITableViewCell subclasses accessible. Is there any better ways to make cells accessible? I mean to make a cell as one accessibility element which will contain all the info for VoiceOver.

why is it turned off by default
A UITableViewCell may be seen as a container inside which many elements are embedded (buttons...) and, as is, you can't have simultaneously a parent view (the table view cell) and its child views (label, button...) that are both accessible with VoiceOver: either your cell can be selected or its content.
By default, the content must be seen by VoiceOver: add two buttons in your cell and you'll see the difference by enabling/disabling the accessibility of the cell.
Is there any better ways to make cells accessible? I mean to make a cell as one accessibility element which will contain all the info for VoiceOver.
To reach your goal, the best way is to make your cell accessible while providing an accessibilityLabel and adding custom actions if many actions are planned in this cell (with buttons for instance).
Following this rationale improves the user experience: one unique selection with possible actions.
If you don't want the elements inside each cell to be read out, just define each one of your cells as follows:
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier",
for: indexPath)
cell.isAccessibilityElement = true
cell.accessibilityLabel = "APPEND YOUR LABELS HERE"
// Add everything you need to construct your cell here.
return cell
}
That's the simplest configuration but you may decide to reach every elements in the cell and, in this case, it's quite different: see this answer or this one if you need some implementation examples for this.
Personnally, I always subclass the table view cell to define its trait, its array of accessiblity elements and its potential actions so as to control the way information are provided to the VoiceOver user: I find it very flexible even if it seems to be tedious at first sight. ;o)

Related

How I can hide button with size in UITableViewCell cell?

I have UITableView and UITableViewCell.
I get data from API. Some items have a link, others have not.
If the item has not to link I wand to hide button with a book icon.
When I use this method (look below) button is hidden right, but then when the tableview reuse this cell icon with a book does not come back. How I can fix it?
var addButtonTrailingConstraint = openPdfButton.widthAnchor.constraint(equalToConstant: 0)
if link == nil{
NSLayoutConstraint.activate([addButtonTrailingConstraint])
}else{
NSLayoutConstraint.deactivate([addButtonTrailingConstraint])
}
}
This is kinda hard to answer without more code / knowledge of your constraint setup.
But I can give you 2 tips how to solve this issue by taking another approach:
1. Approach: Use UIStackView to manage your buttons:
Remove your buttons and replace them with a UIStackView. Then in code, where you config your cell (set text, title, ...) you first remove all Buttons from the UIStackView (you can do this easily with stackView.removeAllArrangedSubviews(), this is needed because the cells are getting reused and you don't want to add more and more buttons every time the cell is getting displayed.
After that, add the buttons you need in this cell (e.g.: like this: stackView.addArrangedSubview(button)).
This approach has the benefit that it is very dynamic, you can add as many different buttons as you wish without having to modify your code.
But since you need to create new buttons all the time it is not the most performance efficient solution.
2. Approach: Use 2 different UITableViewCell classes:
Make 2 different UITableViewCells, one with one button and a second one with 2 buttons. You can also inherit one from another to reduce duplicate code.
Then in tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) check which one of the 2 cell classes you need, create the right one and set its members (text, title, ...).
This approach is less flexible but more performance efficient in comparison to the 1. approach.
I use both approaches in production and they are working quite nicely :)
You need
if link == nil {
openPdfButton.widthAnchor.constraint(equalToConstant: 0).isActive = true
} else {
openPdfButton.constraints.forEach {
openPdfButton.removeConstraint($0)
}
}

Is it language appropriate to layer a UIButton over a UILabel in Swift 3.0?

My question is this: Is this following approach to making a UILabel tap sensitive stylistically acceptable in the Swift 3 language? I'm tempted to say "if it compiles it flies" but I don't want to get in the habit of using this "short cut" if it is going to bite me later on. Further, if it is acceptable, are there some drawbacks to using this method that aren't obvious to a newcomer like me?
Please note that I am not looking for a way to implement code, I have one. I am asking if the solution I have is acceptable from a language style perspective.
I've been trying to get a UILabel to accept a TapGesture when inside a table cell for 2 days now and whatever method I try, there is always some sort of error even if it will compile. On a hunch, I went to my storyboard for the table view and added a button on top (not stacked, or aligned, or anything like that – actually occupying the same 2D space on the story board) within the prototype cell. I deleted the button text, linked it to the table view cell code and implemented some basic functionality to change the text on the UILabel to red and back. All of this functions exactly like I expected. I click the button and the text changes from black to red, and back when clicked again. The UILabel text is static in the table cell on a white background and my real function isn't going to change the text, it is going to alter another view through delegation from that view.
Why do it this way? Even if I check the UILabel use interaction box and follow some of the other questions and answers here to make it tap-able, I cannot control+drag the UILabel to the table view cell and make an action, the option simply doesn't exist in the pull down menu. It is available if I control+drag the UILabel to the table view controller. This makes a kind of sense to me because it is the table view controller that senses touch (right?). But, on the other hand, I have a switch in the table view cell that works just fine when I follow the answer to this question. Simple functionality of the storyboard-code interation (control+drag) is preventing me from getting what I want. Maybe control+drag should be allowing me to make an action and doesn't? I don't know. I don't want to use a UIButton alone because the text scaling feature of UILabel is really handy.
If you just want to recognize taps on the table cell, make the class the delegate of the table view
tableView.delegate = self
and implement
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//your code here
}
I'd also appreciate you taking the time to read a bit more about UITableViews. Every UITableViewCell has a label by itself. Consider the following code:
tableView.dataSource = self // this code will ideally be in init
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "lovCell")
cell.textLabel?.text = dummyData[indexPath.row]
return cell
}

Reusing cells with dynamic layout content at runtime

I have a type of UITableViewCell that lets the user add/remove as many UITextViews as they want at run time.
I'm running into issues when trying to reuse/dequeue cells of that type, as sometimes the tableview cells just start overlapping when you scroll up and down. When I dequeue/return the cell, I'm running a setup method (which initiates a teardown method internally first to remove all the previous views), and uses the model to setup/restore all the necessary views and layout constraints.
if let cell = tableView.dequeueReusableCell(withIdentifier: "MultipleContentCell", for: indexPath) as? MultipleChoiceTableViewCell {
cell.setupCellWithModel(model: model)
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
cell.delegate = self
return cell
}
I can't really figure out why that cells sometimes overlap in the tableview, but I'm guessing it has to do with the layout being recreated on the fly. I'm considering not reusing these types of cells and just storing them in a list.
My question is: are reusable cells always suppose to have the same general UIView layout, and only the content changes? Am I not supposed to use reuse these types of cells? Or has someone experienced this before?
Thanks
The UITextView are created each time you dequeue cell and never delete. To repair that use function prepareForReuse(). You have to define, what your cell should do before dequeue in MultipleChoiceTableViewCell. For example:
override func prepareForReuse() {
super.prepareForReuse()
for view in speciesName.subviews {
if view is UITableView {
view.removeFromSuperview()
}
}
}
I added similar question few days ago:
Cells in UITableView overlapping. Many cells in one place
If you have some question, I can try to help you more tomorrow.
Cheers!
In general, yes. You want the physical layout of your cells to be static, and only vary the contents when you recycle them. If you add views to your cells in cellForRow(at:) then the burden is on you to manage the extra fields to avoid duplicate views.
Your case where you add a variable number of views to a table view cell based on user interaction is an odd case where you might need to add and remove cells on the fly.
One way to handle this would be to put all of your text fields in a container view, add an outlet to that container view, and then simply use code like this in your prepareForReuse or cellForRowAt function:
containerView.subviews.forEach { $0.removeFromSuperview() }

Should I use dequeReusableCellWithIdentifier?

I know this question is dumb. But suddenly got stuck up with this question.
When I use dequeReusableCellWithIdentifier, the cells are reused. To be more specific, first 'n' set of cells is reused - along with their references
As the same references are reused, I can't actually store local variables to the cells. I actually need to assign them everytime in cellForRowAtIndexPath
Assume I'm using a custom complex UITableviewcell. (I know we should reduce complexity. But still...)
Some views are to be added to the cell.
Best example i could think of is an image slider.
So the number of images in the image slider differs based on the datasource. So i need to add the views in cellForRowAtIndexPath. Can't avoid it.
Since the same reference of cells are reused, I need to add these views everytime cellForRowAtIndexPath is called. I think that is a bit of heavy load.
I thought of using drawRect method, but it does not work with UITableViewAutomaticDimension
If I don't use dequeReusableCell, I will have individual referenced cells, but it will cost the memory and performance.
So what is the best solution?
Can I use dequeReusableCell and still need not rewrite its content?
Edit 1:
By mentioning dequeReusableCell, I did mention dequeueReusableCellWithIdentifier - forIndexPath. I'm sorry for the confusion.
Edit 2:
I feel, I'm not so clear in my question.
Lets consider I have a array in my viewcontroller.
I'm configuring my cell in cellForRowAtIndexPath with dequeueReusableCellWithIdentifier - forIndexPath
So what happens is, everytime i scroll, when the invisible rows become visible, cellForRowAtIndexPath is called.
Lets say I have a image slider with n imageviews. For each cell, this 'n' differs. So I'm forced to draw the cell's view based on its dataSource.
So everytime the tableView calls cellForRowAtIndexPath, the cell is configured again and again.
I want to avoid this to improve performance.
what I do in this case is the following:
I always use dequeReusableCell:, for reasons you already said
I write a cleanCell method in the custom UITableViewCell class in order to clean everything has been already set on the cell (just to make it ready for reuse)
then in the cellForRowAtIndexPath: I configure my cell as desired
That's what I would do: I would move the logic of adding those heavy views inside the cell's class itself. So that in cellForRowAtIndexPath I would just set an object that has to be displayed inside the cell, and cell would create all the necessary views on its own (it's a view layer, after all). So inside cellForRowAtIndexPath it would look something like this:
cell = [tableView dequeueReusableCellWithIdentifier...forIndexPath...];
cell.modelObject = myModelObject;
and that's it.
Then inside the cell class I would implement optimizations for switching between different model objects. Like, you could defer releasing the heavy views, and reuse them, when the cell is reused.
To be honest, I don't get your example with the slider, but let's suppose you have a star rating with 5 stars, which can be visible or not. There are various way to do it, but let's assume you're just adding / removing a UIImageView for each star. The way you do it now, you would have an empty cell and create / add views in cellForRow. What I'm suggesting is to have 5 UIImageViews as part of your cell, and inside the cell set their visibility based on the modelObject.rating. Something like that. I hope, it helps to illustrate my point :)
UPDATE: when your cell can have an arbitary number of images inside of it, I would probably create them inside the cell's class. So, for instance, for the first model object we need 3 image views. So we create them. But then we don't release them in prepareForReuse, we wait for the next model object to come. And if it has, say, 1 image, we release 2 image views (or we don't, so that we didn't have to recreate them later, it depends on what is more critical: performance, or memory usage), and if it needs 5, we create two more. And if it needs 3, we're all set already.
In your case you should use dequeueReusableCellWithIdentifier(_ identifier: String, forIndexPath indexPath: NSIndexPath) method. This method automatically decides whether the cell needs to be created or dequeued.
I need to add these views everytime cellForRowAtIndexPath is called. I think that is a bit of heavy load.
You don't need to add those views every time cellForRowAtIndexPath gets called. Those views should be already added as a cell is instantiated. In cellForRowAtIndexPath you only assign images and other values to those views.
In case if you use storyboards and prototype cells your code should look like this:
var items = [UIImage]()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ImageCell", forIndexPath: indexPath)
let item = items[indexPath.row]
let recipient = item.recipients.first
// I have given my UIImageView tag = 101 to easily access it from the view controller.
// This allows me to avoid defining a custom UITableViewCell subclass for simple cells.
if let imageView = cell.viewWithTag(101) as? UIImageView {
imageView.image = item
}
return cell
}
1)I think you need to use "lazy load" of cells.
http://www.theappguruz.com/blog/ios-lazy-loading-images (for example)
2) You can create a property in your viewController, and create an NSMutableDictionary property, which will store the some data by the some key,
if your model have things like "ID" of the cells or you can use indexPath.row for it, then you can load it only one time, and next time when -cellForRow will be called, you can retrieve data from your dictionary by the
[self.dataDictionary objectForKey:#"cellID"];
And with this you, solve your repeating load.
In -cellForRow you can set the check,like.
if ([self.dataDictionary objectForKey:#"cellID"]) {
cell.cellImage = [self.dataDictionary objectForKey:#"cellID"];
} else {
UIImage *img = [uiimage load_image];
cell.cellImage = img;
[self.dataDictionary setObject:img forKey:#"cellID"];
}
And so on. Its like example, the dataType's you can choose by yourself, it's example.
Thanks for everyone helping me out. I think the better answer was from #Eiko.
When you use reusable cells, it must be completely reusable. If you are going to change its views based on the data, you either have to use a new identifier or avoid using reusable cells.
But avoiding reusable cells, will take a load. So you should either use different identifiers for similar cells or as #FreeNickName told - you can use an intermediate configuration and alter it on your need.

Swift grouped UITableView best practices

I'm setting up the settings page of my first Swift app and can't seem to find a clean way to set up the master-detail relationship between the settings view and a child table view of options.
My settings page is a UITableViewController with a custom layout of grouped cells. I also have a detail UITableViewController that I want to use to present options for various settings. My initial thinking led me to think that I can use a single view controller for this and just change the cells based on the segue. My largest problem is loading all the data into the cells since the grouped tables don't have prototype cells that I can call in
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath) {}
Is there a commonly accepted way to set up these sort of settings table, that have a defined number of rows? I could make a viewcontroller for each setting, but that feels wasteful.
Even though it feels wasteful, it's more intuitive and less convoluted to have (segues to) different view controllers.
Your detail view controller has to
display options
return the selected option
save state
So, there are three points where you'd end up with large blocks of conditional code to determine which group of options you're dealing with, while displaying, unwinding, or preserving/restoring details.
If you added more options, a monolithic approach would only continue to grow more and more complex.
A monolithic approach is also the least flexible, if you ever needed to support a second (non-check mark) type of selection in the future.
Separating responsibilities into different (subclassed) detail view controllers means each controller can clearly and simply handle its own details. Your code will be easier to understand, maintain, and update down the road.

Resources