I'm building an app, where I got several sections in an UITableView. My current solution is collecting my data in a dictionary, and then pick one key for every section. Is there a better solution?
One of the good ways to it - direct model mapping, especially good with swift enums.
For example you have 2 different sections with 3 different type of rows. Your enum and ViewController code will look like:
enum TableViewSectionTypes {
case SectionOne
case SectionTwo
}
enum TableViewRowTypes {
case RawTypeOne
case RawTypeTwo
case RawTypeThreeWithAssociatedModel(ModelForRowTypeNumberThree)
}
struct ModelForRowTypeNumberThree {
let paramOne: String
let paramTwo: UIImage
let paramThree: String
let paramFour: NSData
}
struct TableViewSection {
let type: TableViewSectionTypes
let raws: [TableViewRowTypes]
}
class ViewController: UIViewController, UITableViewDataSource {
var sections = [TableViewSection]()
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].raws.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let raw = sections[indexPath.section].raws[indexPath.row]
switch raw {
case .RawTypeOne:
// Here return cell of first type
case .RawTypeTwo:
// There return cell of second type
case .RawTypeThreeWithAssociatedModel(let modelForRawTypeThree):
// And finally here you can use your model and bind it to your cell and return it
}
}
}
What benefits? Strong typization, explicit modelling, and explicit handling of your various cell types. The only simple thing that you have to do in that scenario it is parse your data into this enums and structs, as well as you do it for your dictionaries.
Here is a quick example that I wrote. Please note, it error-prone since it is not checking wether the keys exists not does it create a proper cell.
You could do this with a dictionary as well, since you can iterate over its content.
Hope it helps:
class AwesomeTable: UITableViewController {
private var tableContent: [[String]] = [["Section 1, row 1", "Section 1, row 2"], ["Section 2, row 1", "Section 2, row 2"]]
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return tableContent.count
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableContent[section].count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
let item = tableContent[indexPath.section][indexPath.row]
cell.textLabel?.text = item
return cell
}
}
Implement the table view datasource as follows:-
1) Set number of sections = no of keys in dictionary
2) No of rows in section = no of values in dictionary at index(section)
Related
I have a data source in this form:
struct Country {
let name: String
}
The other properties won't come into play in this stage so let's keep it simple.
I have separated ViewController and TableViewDataSource in two separate files. Here is the Data source code:
class CountryDataSource: NSObject, UITableViewDataSource {
var countries = [Country]()
var filteredCountries = [Country]()
var dataChanged: (() -> Void)?
var tableView: UITableView!
let searchController = UISearchController(searchResultsController: nil)
var filterText: String? {
didSet {
filteredCountries = countries.matching(filterText)
self.dataChanged?()
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredCountries.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let country: Country
country = filteredCountries[indexPath.row]
cell.textLabel?.text = country.name
return cell
}
}
As you can see there is already a filtering mechanism in place.
Here is the most relevant part of the view controller:
class ViewController: UITableViewController, URLSessionDataDelegate {
let dataSource = CountryDataSource()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.tableView = self.tableView
dataSource.dataChanged = { [weak self] in
self?.tableView.reloadData()
}
tableView.dataSource = dataSource
// Setup the Search Controller
dataSource.searchController.searchResultsUpdater = self
dataSource.searchController.obscuresBackgroundDuringPresentation = false
dataSource.searchController.searchBar.placeholder = "Search countries..."
navigationItem.searchController = dataSource.searchController
definesPresentationContext = true
performSelector(inBackground: #selector(loadCountries), with: nil)
}
The loadCountries is what fetches the JSON and load the table view inside the dataSource.countries and dataSource.filteredCountries array.
Now, how can I get the indexed collation like the Contacts app has without breaking all this?
I tried several tutorials, no one worked because they were needing a class data model or everything inside the view controller.
All solutions tried either crash (worst case) or don't load the correct data or don't recognise it...
Please I need some help here.
Thank you
I recommend you to work with CellViewModels instead of model data.
Steps:
1) Create an array per word with your cell view models sorted alphabetically. If you have data for A, C, F, L, Y and Z you are going to have 6 arrays with cell view models. I'm going to call them as "sectionArray".
2) Create another array and add the sectionArrays sorted alphabetically, the "cellModelsData". So, The cellModelsData is an array of sectionArrays.
3) On numberOfSections return the count of cellModelsData.
4) On numberOfRowsInSection get the sectionArray inside the cellModelsData according to the section number (cellModelsData[section]) and return the count of that sectionArray.
5) On cellForRowAtindexPath get the sectionArray (cellModelsData[indexPath.section]) and then get the "cellModel" (sectionArray[indexPath.row]). Dequeue the cell and set the cell model to the cell.
I think that this approach should resolve your problem.
I made a sample project in BitBucket that could help you: https://bitbucket.org/gastonmontes/reutilizablecellssampleproject
Example:
You have the following words:
Does.
Any.
Visa.
Count.
Refused.
Add.
Country.
1)
SectionArrayA: [Add, Any]
SectionArrayC: [Count, Country]
SectionArrayR: [Refused]
SectionArrayV: [Visa]
2)
cellModelsData = [ [SectionArrayA], [SectionArrayC], [SectionArrayR], [SectionArrayV] ]
3)
func numberOfSections(in tableView: UITableView) -> Int {
return self.cellModelsData.count
}
4)
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionModels = self.cellModelsData[section]
return sectionModels.count
}
5)
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let sectionModels = self.cellModelsData[indexPath.section]
let cellModel = sectionModels[indexPath.row]
let cell = self.sampleCellsTableView.dequeueReusableCell(withIdentifier: "YourCellIdentifier",
for: indexPath) as! YourCell
cell.cellSetModel(cellModel)
return cell
}
I use enum for building my UITableView cells:
enum VAXSections: Int
{
case Segmented = 0
case Scrollable = 1
case ScheduledMode = 2
case ScheduledFooter = 3
case SilentHours = 4
case SilentFooter = 5
}
here how I use it:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
guard let unwrappedSection = VAXSections(rawValue: indexPath.section) else {
showFatalError()
return nil
}
Few problem here I want to guard my section value if it's out of max case in my enum. For example if indexPath.section will be bigger then 5 then app should fall back. But if you see we can't return nil here, as cellForRowAtIndexPath has to return cell in any case.
I can solve problem by providing more readability with replacing showFatalError() fiction with:
guard let unwrappedSection = VAXSections(rawValue: indexPath.section) else {
fatalError("Check \(String(VAXUnitDashboardViewController.self)) UITableView datasource or check \(String(VAXSections.self)) enum.")
}
then I don't need to return any value. But then I turned in another problem. As I need to specify at least 3 datasource functions of my UITableView I need to duplicate fatal error which I wish to replace with one function that do the same all the time:
fatalError("Check \(String(VAXUnitDashboardViewController.self)) UITableView datasource or check \(String(VAXSections.self)) enum.")
enum VAXItems: String {
case Item1
case Item2
case Item3
}
enum VAXSections: String {
case Segmented
case Scrollable
case ScheduledMode
case ScheduledFooter
case SilentHours
case SilentFooter
}
struct VAXModel {
var type: VAXSections
var items: [VAXItems]
}
Then on your UIViewController you can have:
class ViewController: UIViewController {
private var model: [VAXModel] = []
override func viewDidLoad() {
super.viewDidLoad()
model = [
VAXModel(type: .ScheduledMode, items: [.Item1, .Item2, .Item3]),
VAXModel(type: .SilentFooter, items: [.Item1])
]
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return model.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model[section].items.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(String(UITableViewCell), forIndexPath: indexPath)
let item = model[indexPath.section].items[indexPath.row]
switch item {
case .Item1: cell.textLabel?.text = item.rawValue
case .Item2: // Config
case .Item3: // Config
}
return cell
}
}
I don't think you need to have it in 3 places actually. Assuming that the 3 data source methods you are talking about are :
func numberOfSectionsInTableView(_ tableView: UITableView) -> Int
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
You actually need to have your guard in only one of them.
numberOfSectionsInTableView is the first method that will be called, so if you fail here, the other two methods won't be called. If the number of sections is based on some calculations, you can also cut off the value, something like this : if calculatedNumberOfSections > 6 { return 6 } else { return calculatedNumberOfSections } (remember that section numbering is 0 based)
numberOfRowsInSection - if you guard here you have two options - either fail with fatalError or (better in my opinion) - return 0 if a section number higher than 5 gets passed. Returning 0 will result in cellForRowAtIndexPath not being called with that section.
cellForRowAtIndexPath - you already got this :)
I know that I can query for, lets say, users that have emailVerified equal to true and present them into a tableView, but I was having trouble getting a single Parse object of type array into a tableView. I couldn't find anything online about this specific problem, but after putting a few answers together, I got it to work my answer is below for those also having trouble with this.
Here is what I found based on my question. I have an object in Parse called "my_classes" that is of type array. I want to get the items from the array into a tableView.
1) Create a variable: var myClassesResults : NSMutableArray = []
2) Create the function or place the code where necessary:
func getUserData() {
if PFUser.currentUser()!.objectForKey("my_classes") != nil {
let classes = PFUser.currentUser()!.objectForKey("my_classes")!
myClassesResults = classes as! NSMutableArray
self.noClasses = false
self.tableView.reloadData()
} else {
self.noClasses = true
self.tableView.reloadData()
}
}
3) tableView functions:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.myClassesResults.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel!.text = myClassesResults[indexPath.row] as? String
return cell
}
This has been killing me for a few hours now. I have a UITableViewController that has multiple data sections. My data source is simply an Array.
The problem I'm running into is that each section is repeating data from the array starting from the first index instead of "slicing" it as I expect it should.
Simplified example:
let sections = ["Section A", "Section B"]
let counts = [3, 5]
let source = ["a","b",c","d","e","f","g","h"]
// Output in simulator:
# Section A
- a
- b
- c
# Section B
- a
- b
- c
- d
- e
- and so on...
I would expect that "Section B" would be the next 5 results starting at "d" and not restart from the first index.
The relevant code is pretty standard stuff:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return sections.count // returns 2
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return counts[section] // returns correct data
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let data = source[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! MyTableViewCell
// some cell formatting, populate UILabels, etc
cell.testLabel.text = data["test"].string
return cell
}
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCellWithIdentifier("Header") as! MyTableViewHeaderCell
headerCell.backgroundColor = UIColor.grayColor()
headerCell.testHeaderLabel.text = sections[section]
return headerCell
}
Initial searching of SO led me to believe it's a cell reuse issue but after overriding prepareForReuse in my cell class, I don't think thats it.
Expected Results
# Section A
- a
- b
- c
# Section B
- d
- e
- f
- g
- h
Like I said, I'm expecting that dividing the TableView data in to sections would keep a reference to the array pointer and continue where it left off instead of starting back at 0 for each section.
Any thoughts or suggestions?
indexPath.row always returns the row-number inside a section.
In your second section, you need to add the number of rows displayed in all sections before.
Change let data = source[indexPath.row] to something like this:
let data = source[indexPath.row+counts[0]]
If you add more sections, this will be a bit more complicated to calculate.
Other idea:
If it is possible, you could rearrange your array. You could make a two-dimensional array. The main array would include arrays with the data for each section.
To display it, you' need to use indexPath.section, too.
dataArray[indexPath.section][indexPath.row]
Using the idea of FelixSFD, but with a little logical modification, so you can work dynamically:
Change this:
let data = source[indexPath.row]
for this:
var countIndex = indexPath.row
for section in 0...indexPath.section {
countIndex += counts[section]
}
let data = source[countIndex]
Be careful with this approach because you may have some performance issues on large tableViews.
If you can rearrange your array:
change
let source = ["a","b",c","d","e","f","g","h"]
into
let source = [["a","b","c"],["d","e","f","g","h"]]
and change
let data = source[indexPath.row]
into
let data = source[indexPath.section][indexPath.row]
I had the same problem, but with more complex situation, and i needed more dynamically way of doing it. Sure i could rearrange my data, to use two-dimensional array, but i don't want to handle it later. So i did it like this.
I am pulling my data from firebase, so i never know, how many sections/arrays i will have.
Creating an array, to insert amount of items in array.
var counterTableView = [Int]()
Filling array with 0, without doing it, i was getting errors later. (Index out of range)
override func numberOfSections(in tableView: UITableView) -> Int {
for i in 0...Array(Set(self.sections)).count {
counterTableView.insert(0, at: i)
}
counterTableView.removeLast(counterTableView.count-Array(Set(self.sections)).count-1)
return Array(Set(self.sections)).count
}
Next step, is to fill the amount of items in one section in array
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
counterTableView[section+1] = counts[section] + counterTableView[section]}
Last step, showing the data in cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
cell.textLabel.text = source[indexPath.row+counterTableView[indexPath.section]]}
I'd like to get started using swift to make a small list based application. I was planning on using two table view controllers to display the two lists, and was wondering if it were possible to have them share a common data source.
Essentially the data would just be an item name, and two integers representing the amount of the item owned vs needed. When one number increases, the other decreases, and vice versa.
I figured this might be easiest to do using a single data source utilized by both table view controllers.
I did some googling on shared data sources and didn't find anything too useful to help me implement this. If there are any good references for me to look at please point me in their direction!
You can create one data source class and use it in both view controllers:
class Item {
}
class ItemsDataSource: NSObject, UITableViewDataSource {
var items: [Item] = []
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
//setup cell
// ...
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
}
class FirstViewController : UITableViewController {
var dataSource = ItemsDataSource()
override func viewDidLoad() {
self.tableView.dataSource = dataSource
self.tableView.reloadData()
}
}
class SecondViewController : UITableViewController {
var dataSource = ItemsDataSource()
override func viewDidLoad() {
self.tableView.dataSource = dataSource
self.tableView.reloadData()
}
}
use singleton design pattern, it means both tables will get data source from the same instance
class sharedDataSource : NSObject,UITableViewDataSource{
static var sharedInstance = sharedDataSource();
override init(){
super.init()
}
//handle here data source
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
}
}
var tableOne = UITableView();
var tableTwo = UITableView();
tableOne.dataSource = sharedDataSource.sharedInstance;
tableTwo.dataSource = sharedDataSource.sharedInstance;
The first argument to the delegate method is:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
}
At that point, your one Datasource delegate can decide which table view is wanting a cell, for example, and return results accordingly.