Swift rendering static cells with dynamic content - ios

I have a static UITableView with n columns and 1 section. The cells are detailed with some dynamic content passed from another UIViewController.
My solution is the following: I make a call in viewDidAppear to present the dynamic content and eventually reload the view:
override func viewDidAppear(_ animated: Bool) {
setupDetails()
}
fileprivate func setupDetails() {
tableView.cellForRow(at: [0,0] as IndexPath)?.detailTextLabel?.text = "..."
tableView.cellForRow(at: [0,1] as IndexPath)?.detailTextLabel?.text = "..."
// .... multiple cells
tableView.reloadData()
}
The thing I don't like is that the content is updated after the view is shown to the user (thanks to the viewDidAppear call).
For what concerns working on the cellForRow() method, I could not have it working given the fact that the cell is statically created in my Storyboard.
My question is if there exists a pattern to be used in this specific case which I didn't think of. I basically want the view to be loaded before the view is presented, as if I was working with dynamic cells.

My question is if there exists a pattern to be used in this specific case
Yes. Don't use a static table. Your table is not static! "Static" means "never changes". Your table is the opposite of static; it does change. It is dynamic!
So, use an ordinary dynamic table. Maintain a data model that is consulted by the Three Big Questions (the data source methods). To update the table with new data, first update the data model, and then call reloadData. That will cause cellForRowAt to be called for you, and you'll populate each cell from the data model. That is much more efficient than what you're doing.
As for the flash, that's just a matter of timing; use viewWillAppear instead of viewDidAppear.

Don't use ViewDidAppear if you don't want you change to show to the user. Use ViewWillAppear and the view will already be in its final state when the user first sees it.

Related

Working with TableViews and CollectionViews and managing multiple data sources in Swift

I have a main ViewController, MainViewController that contains a number of views along with a TableView.
Each row (i.e. tableViewCell) contains different content from different sources/views etc. A number of those rows in the tableView, in turn, contain a CollectionView called SettingsCollectionView. In the tableView's cellForRowAt method, I initialize the SettingsCollectionView for that tableViewCell and also pass it the respective data which that CollectionView subclass uses as its data source. So for example:
tableView row 0 - contains generic content
tableView row 1 - settingsCollectionViewA : SettingsCollectionView as well as a struct OriginalDataA
tableView row 2 - contains generic content
tableView row 3 - settingsCollectionViewB : SettingsCollectionView as well as a struct OriginalDataB
tableView row 4 - settingsCollectionViewC : SettingsCollectionViewas well as a struct OriginalDataC
tableView row 5 - contains generic content
When I initialize settingsCollectionViewA with OriginalDataA, I have a setter in that CollectionView that then sets up the local data under LocalDataA. This allows me to ensure I have the original and the working copy of the data based on the user making changes etc. Any time I call the OriginalDataA variable from the MainViewController, a getter in settingsCollectionViewA does some cleaning up of the data etc. so I can then do what I want with it in the MainViewController.
That part all works well except if those tableView cells are dequeued, when they reappear, I get back the original state for that tableViewCell and in turn collectionView rather than the state the user left it in.
I realize this is because each SettingsCollectionView class is working its own local copy of OriginalDataA, OriginalDataB etc. and appreciate I can just update the original data but then that creates other complexities - like the cleanness of current 'standalone' code built for the SettingsCollectionView subclass as well as the complexity of original vs. updated data. That's why I am stuck on a better programming approach...
Apologies, this might seem like a basic question but I'm new to programming and all the examples etc. I can find all speak to more simple scenarios rather than what I'm trying to do.
I haven't included the code because it's got a whole lot of other content and functionality that I think just confuses the concept outlined above.
Any help would be greatly appreciated.
Create a class that holds each of your data structs:
class MyDataModel {
var dataA: DataA
var dataB: DataB
var dataC: DataC
}
Then you create an instance of MyDataModel and hold a reference to it in a property of your view controller. Pass this same instance to your table view cells. Since it is a class and therefore a reference type, changes made by the cell will actually be made in this one instance.
Consider you have SettingsCollectionViewA which shows originalDataA. CollectionView displays the data what you provide in cellforrowatindex method. Collection view cells reuse memory every time. Only visible cells stays in the memory at any point of time. So user modified data will not be stored explicitly unless you modify the originalDataA.

Swift Button That Outputs UICollectionView Cells One by One?

I made a UICollectionView, and everything is working. It makes 100 cells that I can scroll through in simulator with no problem.
However, rather than seeing all the cells at once, I want the cells to be released one by one whenever that red button is pressed.
I am confused because I noticed in the storyboard, it hard codes the number of cells it has on the screen at once. Is there any way to get around this?
Thank you!
This is what the UI looks like in storyboard.
This is the code I used to make it. It's basic, and just says to fill the text box of the cell with a string from the array.
Your question is garbled.
A collection view has a delegate and a data source. The data source responds to messages in the UICollectionViewDataSource protocol. That protocol lets the collection view ask how many sections it has, and how many rows in each section, as well as asking for the cells from those sections and rows.
There are also methods that let you tell the table view that you want to add more cells. Take a look at the method insertItems(at:). That lets you provide an array of indexPaths, which tells the table view that you have added new entries.
You could certainly write a button action method that added one or more entries to your data model and then used the insertItems(at:) method to notify the collection view that it had new entries. If there was room in the content view of the collection view to display additional cells it would then call the data source and ask for new cells at those index paths.
Sounds like you just need to keep track of how many items you want displayed (which will increase the more that button is pressed) and use that in your UICollectionViewDataSource method. Something like:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return min(myRunningItemCount, maximumNumberOfItems) //assuming there's a maximum
}
Then you just need to call reloadData on the collection view whenever that number changes.

Collection view's last focused index path lost after reload data in tvOS

In my tvOS app, I have a collection view where I've set its remembersLastFocusedIndexPath to true. As this is not enough to get this behaviour, I've also overridden a method in my UIViewController like this:
override weak var preferredFocusedView: UIView? {
return collectionView
}
This was working fine until I started reloading the collection view for some reasons. It does work if, being the collection view visible, I call collectionView.reloadData().
This doesn't work, tho, if I do the reload when the collection view is not visible, for example, when I'm in a detail view after tapping one of the items. When I come back to the grid, the focused index path is not the one I tapped.
As a workaround, I'm managing this non-ideal scenario:
As the reload is triggered by fresh data coming from my backend, I only call collectionView.reloadData() when the collection view is visible (because I know that the last focused index doesn't change here).
I call collectionView.reloadData() in viewDidAppear(animated: Bool) to have the latest content available when the user comes back.
How can I do this properly? Thanks in advance.
Recently I got this issue also, my situation is pretty much same to you.
I also called collectionView.reloadData() in viewDidLoad and viewWillAppear
And then
I changed from
collectionView.reloadData()
to
collectionView.reloadItemsAtIndexPaths(collectionView.indexPathsForVisibleItems())
It works for me most of time, there is still approximately <5% chance to mess up the last focus index
One option is to set remembersLastFocusedIndexPath to false, and implement indexPathForPreferredFocusedViewInCollectionView: to achieve the same behavior.

It is possible to maintain two tableviews and one viewcontroller in segmented controller?

I have a doubt how to maintain two tableviews and one view controller in segmented controller. In segment=0 I want display first tableview, In segment=1 to display second tableview and segment=2 to display one view controller, is it possible?
Yes. it is possible. There are two ways.
Use single tableview. And based on segment, change the tableview values.
Use two custom tableview class, and custom Protocal/delegate methods to manage the events.
You really don't need to have 2 Tableviews. Based on the segment selection call [tableview reloaddata] with your data what to display on that segment section. This saves lot of efforts and manipulation.
I think a scrollView can help you solve you problem:
Just add the two tableViews on the scrollView and use the segmentedController's method to set the scrollView's contentOffset.x
Similarly, you can add the ViewController on the scrollView followed the two tableView and scroll to it by click the segment == 2
Hope that my advice can help you :)
Whether you're using one or multiple table views, the main idea is to handle the selection event.
If your segmented control is created in the storyboard, you can use the #IBAction outlet, alternately you can use the 'addTarget' syntax.
e.g:
mySegmentedControl.addTarget(self, #selector(myHandlerMethod(_:), for: .valueChanged)
where the handler method is something like:
(first case)
#IBAction func myHandlerMethod(_ sender: UISegmentedControl) {
}
(second case)
#objc func myHandlerMethod(_ sender: UISegmentedControl) {
if sender.selectedSegmentIndex == 1 {
// handle your table view reloading or switching between first and second table views.
} else {
// otherwise, do something else
}
}
Some additional thoughts:
I would agree with others in using a single table view and giving it a different set of data. You can use different subclasses of UITableViewCell to represent your data if the content format between your data sets is different in structure and meaning.
It might also be worth looking into using child view controllers instead of plain views/table views. That way you can encapsulate logic relating to each specific scene you want to present inside a view controller and manage it independently.

UITableView with: Static Cells with dynamic content and dynamic cells

I have a UITableView that always has 4 sections. The first section always has 1 row, the second section always has 3 rows. Then the 3rd and 4th section can have any number of rows. Although the first two sections are essentially static cells, the content in them changes.
The first two sections are all I had to begin with so I just used static cells with outlets to the content directly in the TableViewController. Then I added two more sections which can have any number of rows.
I found that to do this, now I have to consider the whole table as using dynamic prototypes and I have to implement all the delegate/datasource classes for every cell now. In addition to that, I can no longer have my outlets (even for the first 2 sections) directly in the tableViewController but instead create custom subclasses for them so I can change their content.
This seems silly because they are so simple. Is there a better way I can do this? Maybe I can have two tableViews in the TVC, one that is static and handled the same way for the static cells and one that uses dynamic prototypes? If that's the best way, how should I implement that? I have never implemented a TableViewController before that handles more than 1 TableView.
For reference, the View is a profile view with simple data about the person's profile. The first 2 sections are just the person's picture, name, email, and description.
The second 2 sections contain 1) a list of favorited postings on the app. And 2) a list of postings that the user has created.
Here are 2 pictures showing the view:
No, fortunately Apple did add static table cell for UITableViewController but the road ends there. If you want to start modifying content you need to switch to a "real" UITableView, setup arrays and handle the cells correctly.
2 options.
Create IBOutlets to the static cells content and change the objects directly.
or...
Setup the tableView delegate methods and change content using the UITableView delegate and datasource.
class CustomCell: UITableViewCell {
#IBOutlet weak var labelName: UILabel!
}
In reference to the comment about getting the indexPath of a button click from the viewController check out the following code. Basically I created an extension of UITableView that includes the indexPathForView function. This way you can get the exact view(button) you pressed to handle any actions from your UIViewController. I prefer this technique over creating a protocol/delegate on the cell then calling back to the viewController.
#IBAction func pressedItem(sender: AnyObject) {
let button = sender as! UIButton
var indexPath = self.tableView.indexPathForView(button)!
let object = self.arrayOfObjects[indexPath.row]
// Now you can fire any action and have it related to that specific cell
// Create a HelpfulExtensions.swift class to hold your convenience extensions such as this
// MARK: - UITableView
extension UITableView {
func indexPathForView (view : UIView) -> NSIndexPath? {
let location = view.convertPoint(CGPointZero, toView:self)
return indexPathForRowAtPoint(location)
}
}
//
And might I add, EXTENSIONS ROCK!!!
Instead of using two tableViews it may be enough to setup a header view with static content. Probably it's easy to do inside the same and only one table view controller. But also it can be done separately and then a table view below with dynamic content, all within a simple view controller

Resources