Generics in Swift - swift

I am learning about Generics in Swift. For me, this topic is quite hard to understand. In the book I am reading, there is 2 challenges on Generics:
1st challenge: it asks to write a function findAll(_:_:) that takes and array of any type T that conforms to the Equatable protocol and a single element (also of type T). findAll(_:_:) should return an array of integers corresponding to every location where the element was found in the array. For example, findAll([5,3,7,3,9], 3] should return [1,3].
2nd challenge: to modify findAll(_:_:) to accept a Collection instead of an array and it gives a hint "You will need to change the return type from [Int] to an array of an associated type of the Collection protocol"
This is what i have done for first challenge
func findAll<T:Equatable> (_ first: [T], _ second: T) -> [Int] {
var array = [Int]()
for i in 0..<first.count {
if first[i] == second {
array.append(i)
}
}
return array
}
For the second challenge, what i am thinking about is a generic function that I can pass a Collection (can be an Array, a Dictionary or a Set). But for Set type, as it does not have a defined ordering, how do you find location of an item in a Set?
Thank you.

The subscript method of collections is defined as
public subscript(position: Self.Index) -> Self.Iterator.Element { get }
which means that your function should take as arguments
a collection C, and
a value of the associated type C.Iterator.Element
and return an array of C.Index. In addition, the element type
should be Equatable:
func findAll<C: Collection> (_ collection: C, _ element: C.Iterator.Element) -> [C.Index]
where C.Iterator.Element: Equatable
{ ... }
Similar as in your solution for arrays, one can loop over the
collection's indices:
func findAll<C: Collection> (_ collection: C, _ element: C.Iterator.Element) -> [C.Index]
where C.Iterator.Element: Equatable
{
var result: [C.Index] = []
var idx = collection.startIndex
while idx != collection.endIndex {
if collection[idx] == element {
result.append(idx)
}
collection.formIndex(after: &idx)
}
return result
}
One would expect that something like
for idx in collection.startIndex ..< collection.endIndex
// or
for idx in collection.indices
works, but (in Swift 3) this requires an additional constraint
on the associated Indices type:
func findAll<C: Collection> (_ collection: C, _ element: C.Iterator.Element) -> [C.Index]
where C.Iterator.Element: Equatable, C.Indices.Iterator.Element == C.Index
{
var result: [C.Index] = []
for idx in collection.indices {
if collection[idx] == element {
result.append(idx)
}
}
return result
}
This is no longer necessary in Swift 4, see for example
Unable to use indices.contains() in a Collection extension in Swift 3 for a good explanation.
This can now be simplified using filter:
func findAll<C: Collection> (_ collection: C, _ element: C.Iterator.Element) -> [C.Index]
where C.Iterator.Element: Equatable, C.Indices.Iterator.Element == C.Index
{
return collection.indices.filter { collection[$0] == element }
}
Example (a collection of Character):
let chars = "abcdabcdabcd".characters
let indices = findAll(chars, "c")
for idx in indices {
print(chars[idx])
}
Set is a Collection as well, it has an associated Index
type and a subscript method. Example:
let set = Set([1, 2, 3, 4, 5, 6, 7, 8, 9])
let indices = findAll(set, 3)
for idx in indices {
print(set[idx])
}
Finally you might want to define the function as a method
on the Collection type:
extension Collection where Iterator.Element: Equatable, Indices.Iterator.Element == Index {
func allIndices(of element: Iterator.Element) -> [Index] {
return indices.filter { self[$0] == element }
}
}
// Example:
let indices = [1, 2, 3, 1, 2, 3].allIndices(of: 3)

In order to create a custom definition of a function, you should create an extension on the type which the function belongs to. From there, the override tag must be used so that you can create a custom implementation for the function. More specifically, to create a function that accepts a collection instead of an array, create an overridden version of the function that accepts a collection instead.
Also please show us what you've tried so far, instead of just saying I've tried several things.
Here are some links that may be useful:
Swift Documentation
Override function
Another simple example of an override is whenever a ViewContoller class is made, the viewDidLoad() method where view setup happens in is often overridden.

Generics was quite a headache for me when I started learning it in the beginning. Though after some dedicated research in this topic I came across [this] 1 nice tutorial which helped me understanding this topic little deeper. Here I'm sharing the demo code which I'd prepared while learning, hope that help someone.
Demo contains UITableview with different type of cells, each UITableview represents single UITableViewCell with associated Model. I've also added one Hybrid Tableview in order to mix different types of cell in single tableview.
Here is the code.
Creating Generic UITableViewCells first
protocol ProtocolCell {
associatedtype U
static var cellIdentifier:String { get }
func configure(item:U,indexPath:IndexPath)
}
class BaseCell<U>: UITableViewCell, ProtocolCell {
var item:U!
static var cellIdentifier: String {
return String(describing: self)
}
func configure(item:U,indexPath: IndexPath) {
textLabel?.text = "Row: \(indexPath.row), \tClass: \(U.self) OVERRIDE THIS !!"
backgroundColor = .red
}
//MARK:- INIT
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func commonInit(){
selectionStyle = .none
}
}
class StringCell: BaseCell<String> {
override func configure(item: String, indexPath: IndexPath) {
super.configure(item: item, indexPath: indexPath)
textLabel?.text = "Row: \(indexPath.row), \tClass: \(U.self), \tData: \(item)"
backgroundColor = UIColor.yellow.withAlphaComponent(0.3)
}
}
class DogCell: BaseCell<Dog> {
override func configure(item: Dog, indexPath: IndexPath) {
super.configure(item: item, indexPath: indexPath)
textLabel?.text = "Row: \(indexPath.row), \tClass: \(U.self), \tData: \(item.name)"
backgroundColor = UIColor.green.withAlphaComponent(0.3)
}
}
class CountryCell: BaseCell<Country> {
override func configure(item: Country, indexPath: IndexPath) {
super.configure(item: item, indexPath: indexPath)
textLabel?.text = "Row: \(indexPath.row), \tClass: \(U.self), \tData: \(item.name)"
backgroundColor = UIColor.magenta.withAlphaComponent(0.3)
}
}
Then creating Generic UITableViews
class BaseTableView<T_Cell:BaseCell<U_Model>,U_Model>: UITableView, UITableViewDelegate, UITableViewDataSource {
var arrDataSource = [U_Model]()
var blockDidSelectRowAt:((IndexPath) -> Void)?
override init(frame: CGRect, style: UITableViewStyle) {
super.init(frame: frame, style: style)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addIn(view:UIView) {
view.addSubview(self)
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
leftAnchor.constraint(equalTo: view.readableContentGuide.leftAnchor),
rightAnchor.constraint(equalTo: view.readableContentGuide.rightAnchor),
bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
}
func commonInit() {
delegate = self
dataSource = self
backgroundColor = .gray
layer.borderWidth = 2
layer.borderColor = UIColor.red.cgColor
register(T_Cell.self, forCellReuseIdentifier: T_Cell.cellIdentifier)
}
//MARK:- DATA SOURCE
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrDataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: T_Cell.cellIdentifier, for: indexPath) as! BaseCell<U_Model>
cell.configure(item: arrDataSource[indexPath.row], indexPath: indexPath)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
blockDidSelectRowAt?(indexPath)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 50
}
}
class DogTableView: BaseTableView<DogCell, Dog> {
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 70
}
}
class StringTableView: BaseTableView<StringCell, String> {
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 90
}
}
class CountryTableView: BaseTableView<CountryCell, Country> {
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
class HybridTableView: BaseTableView<BaseCell<Any>, Any> {
// OVERRIDING DEFAULT BEHAVIOUR
override func commonInit() {
super.commonInit()
register(DogCell.self, forCellReuseIdentifier: DogCell.cellIdentifier)
register(StringCell.self, forCellReuseIdentifier: StringCell.cellIdentifier)
register(CountryCell.self, forCellReuseIdentifier: CountryCell.cellIdentifier)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let hybridItem = arrDataSource[indexPath.row]
switch hybridItem {
case is Dog:
let cell = tableView.dequeueReusableCell(withIdentifier: DogCell.cellIdentifier) as! DogCell
cell.configure(item: hybridItem as! Dog, indexPath: indexPath)
return cell
case is String:
let cell = tableView.dequeueReusableCell(withIdentifier: StringCell.cellIdentifier) as! StringCell
cell.configure(item: hybridItem as! String, indexPath: indexPath)
return cell
case is Country:
let cell = tableView.dequeueReusableCell(withIdentifier: CountryCell.cellIdentifier) as! CountryCell
cell.configure(item: hybridItem as! Country, indexPath: indexPath)
return cell
default:
let cell = tableView.dequeueReusableCell(withIdentifier: BaseCell<Any>.cellIdentifier) as! BaseCell<Any>
cell.configure(item: hybridItem, indexPath: indexPath)
return cell
}
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let hybridItem = arrDataSource[indexPath.row]
switch hybridItem {
case is Dog: return 70
case is String: return 140
case is Country: return 210
default: return 50
}
}
}
Models used in the tableview
struct Dog {
let name : String
}
struct Country {
let name : String
}
ViewController
class ViewController: UIViewController {
//MARK:- OUTLETS
#IBOutlet private weak var btn: UIButton!
private let tableViewDog = DogTableView()
private let tableViewString = StringTableView()
private let tableViewCountry = CountryTableView()
private let tableViewHybrid = HybridTableView()
//MARK:- VIEW LIFE CYCLE
override func viewDidLoad() {
super.viewDidLoad()
// doSetupUI()
doSetupUIHybrid()
}
//MARK:- UI SETUP
private func doSetupUI(){
tableViewDog.addIn(view: view)
tableViewDog.arrDataSource = [Dog(name: "Dog 1"),Dog(name: "Dog 2")]
// tableView.arrDataSource = ["First","Second"]
// tableView.arrDataSource = [Country(name: "India"),Country(name: "Nepal")]
tableViewDog.reloadData()
tableViewDog.blockDidSelectRowAt = { [unowned selff = self] indexPath in
print("DID SELECT ROW : \(indexPath.row), VALUE : \(selff.tableViewDog.arrDataSource[indexPath.row].name)")
}
}
private func doSetupUIHybrid(){
tableViewHybrid.addIn(view: view)
tableViewHybrid.arrDataSource = [Dog(name: "Dog1"),
"String1",
Country(name: "India"),
Dog(name: "Dog2"),
"String2",
Country(name: "Nepal")]
tableViewHybrid.reloadData()
tableViewHybrid.blockDidSelectRowAt = { [unowned selff = self] indexPath in
var itemToPrint = ""
let hybridItem = selff.tableViewHybrid.arrDataSource[indexPath.row]
switch hybridItem {
case is Dog: itemToPrint = (hybridItem as! Dog).name
case is Country: itemToPrint = (hybridItem as! Country).name
case is String: itemToPrint = (hybridItem as! String)
default: break
}
print("DID SELECT ROW : \(indexPath.row), VALUE : \(itemToPrint)")
}
}
}

Related

Bind UITableView with Combine DataSource

I want to directly link a UITableView with a #Published attribute without using DiffableDataSouce.
If I make the person
struct Person {
let name: String
}
and create the data array:
#Published
var people = [Person(name: "Kim"), Person(name: "Charles")]
So I want to bind my UITableView directly, with something like:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return $people.count()
}
But this gives the error
Cannot convert return expression of type 'Publishers.Count<Published[Person]>.Publisher>' to return type 'Int'
The problem here is that the UITableViewDataSource is pull based (the framework pulls data from your code) but Publishers are push based (they push data to something.) That means that in order to make it work, you need a Mediator (a la the Mediator pattern.)
One option would be to bring in RxSwift/RxCocoa and the RxCombine project to translate between Combine and RxSwift and use the functionality where this already exists. That's a lot for this one ask, but maybe you have other areas where RxCocoa could streamline your code as well.
For just this ask, here is a Mediator that I think would work:
#available(iOS 13.0, *)
final class ViewController: UIViewController {
var tableView: UITableView = UITableView()
#Published var people = [Person(name: "Kim"), Person(name: "Charles")]
var cancellable: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
tableView.frame = view.bounds
tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
view.addSubview(tableView)
cancellable = $people.sink(receiveValue: tableView.items { tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = item.name
return cell
})
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.people = [Person(name: "Mark"), Person(name: "Allison"), Person(name: "Harold")]
}
}
}
extension UITableView {
func items<Element>(_ builder: #escaping (UITableView, IndexPath, Element) -> UITableViewCell) -> ([Element]) -> Void {
let dataSource = CombineTableViewDataSource(builder: builder)
return { items in
dataSource.pushElements(items, to: self)
}
}
}
class CombineTableViewDataSource<Element>: NSObject, UITableViewDataSource {
let build: (UITableView, IndexPath, Element) -> UITableViewCell
var elements: [Element] = []
init(builder: #escaping (UITableView, IndexPath, Element) -> UITableViewCell) {
build = builder
super.init()
}
func pushElements(_ elements: [Element], to tableView: UITableView) {
tableView.dataSource = self
self.elements = elements
tableView.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
elements.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
build(tableView, indexPath, elements[indexPath.row])
}
}

Adding Cells to Dynamic UITableView DataSource | Swift

I have a dynamic UITableViewDataSource that takes in a Model and a UITableViewCell
I would like to insert a different UITableViewCell into the 'nth' (10th for example) row... (for Google Ads)
DATASOURCE
class ModelTableViewDataSource<Model, Cell: UITableViewCell>: NSObject, UITableViewDataSource {
typealias CellConfigurator = (Model, Cell) -> Void
var models: [Model]
private let reuseIdentifier: String
private let cellConfigurator: CellConfigurator
init(models: [Model], reuseIdentifier: String, cellConfigurator: #escaping CellConfigurator) {
self.models = models
self.reuseIdentifier = reuseIdentifier
self.cellConfigurator = cellConfigurator
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = models[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier) as! Cell
cellConfigurator(model, cell)
return cell
}
}
I then add an extension for each Model
MODEL
extension ModelTableViewDataSource where Model == SomeModel, Cell == SomeCell {
static func make(for someModel: [SomeModel], reuseIdentifier: String = "Identifier") -> ModelTableViewDataSource {
return ModelTableViewDataSource(models: someModel, reuseIdentifier: reuseIdentifier) { (someModel, someCell) in
someCell.model = someModel
}
}
}
What is the best way to implement this keeping the re-usable functionality of the UITableViewDataSource
I thought of this:
create a subclass
override numberOfRowsInSection to return number of rows + 1
override cellForRowAt to handle the 10th row
class ModelTableViewDataSourceWithAd: ModelTableViewDataSource {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return super.tableView(tableView, numberOfRowsInSection:section) + 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath.row == 9) {
// return Google ad cell
} else {
return super.tableView(tableView, cellForRowAt:indexPath)
}
}
}
and then the creator:
extension ModelTableViewDataSource where Model == SomeModel, Cell == SomeCell {
// ...
static func makeWithAd(for someModel: [SomeModel], reuseIdentifier: String = "Identifier") -> ModelTableViewDataSource {
return ModelTableViewDataSourceWithAd(models: someModel, reuseIdentifier: reuseIdentifier) { (someModel, someCell) in
someCell.model = someModel
}
}
}

Random Ads between cells

I am trying to put Ads totally randomly between cells inside a UITableView. I am gonna show my main file to you understand what I am doing and how I want:
Table View Controller:
class Page1: UITableViewController, UISearchBarDelegate {
#IBOutlet weak var searchBar: UISearchBar!
var employeesSearching = [Employee]()
var isSearching : Bool = false
#IBOutlet weak var GoogleBannerView: GADBannerView!
let collation = UILocalizedIndexedCollation.current()
var sections: [[Any]] = []
var objects: [Any] = [] {
didSet {
let selector: Selector = #selector(getter: UIApplicationShortcutItem.localizedTitle)
sections = Array(repeating: [], count: collation.sectionTitles.count)
let sortedObjects = collation.sortedArray(from: objects, collationStringSelector: selector)
for object in sortedObjects {
let sectionNumber = collation.section(for: object, collationStringSelector: selector)
sections[sectionNumber].append(object as AnyObject)
}
self.tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.searchBar.delegate = self
self.tableView.contentOffset = CGPoint(x: 0, y: searchBar.frame.height) //hide searchBar
Shared.instance.employees.sort {
(first, second) in
first.name.compare(second.name, options: .diacriticInsensitive) == .orderedAscending
}
}
func getMatches(letter: String, withArray array: [Employee]) -> [Employee] {
return array.filter({ ($0.name.compare(letter, options: .diacriticInsensitive, range: $0.name.startIndex..<$0.name.index($0.name.startIndex, offsetBy: 1), locale: nil) == .orderedSame)})
}
override func numberOfSections(in tableView: UITableView) -> Int {
if isSearching { return 1 }
return collation.sectionTitles.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let letter = collation.sectionTitles[section]
if isSearching {
return employeesSearching.count
} else {
let matches = getMatches(letter: letter, withArray: Shared.instance.employees)
if !matches.isEmpty { return matches.count }
}
return 0
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if isSearching { return nil }
let letter = collation.sectionTitles[section]
let matches = getMatches(letter: letter, withArray: Shared.instance.employees)
if matches.count == 0 { return nil }
return collation.sectionTitles[section] }
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
if isSearching { return nil }
return collation.sectionIndexTitles }
override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
return collation.section(forSectionIndexTitle: index) }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 3 || indexPath.row == 9 || indexPath.row == 14 {
let cellAd = tableView.dequeueReusableCell(withIdentifier: "cellAd", for: indexPath)
GoogleBannerView?.adUnitID = "ca-app-pub-6043248661561548/4628935113"
GoogleBannerView?.rootViewController = self
GoogleBannerView?.load(GADRequest())
return cellAd
}
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell1
if isSearching {
cell.nameLabel.text = employeesSearching[indexPath.row].name
cell.positionLabel.text = employeesSearching[indexPath.row].position
} else {
let letter = collation.sectionTitles[indexPath.section]
let matches = getMatches(letter: letter, withArray: Shared.instance.employees)
cell.nameLabel.text = matches[indexPath.row].name
cell.positionLabel.text = matches[indexPath.row].position
}
return cell
}
...
}
How do I smuggle a UITableViewCell as! AdCell randomly into the UITableView?
I mean, what should I do in cellForRowAt? I am a bit confused between all these indexed sections.
Firstly you need to generate a random number between 0 and your tableView Datasource array size
let lower : UInt32 = 0
let upper : UInt32 = array.count
let randomIndex = arc4random_uniform(upper - lower) + lower
then you need to add the Ad object in the array at the randomIndex
array.insert(AdObject, atIndex:randomIndex)
then just reload your tableView and handle the different types in cellForRow function
One approach would be to insert some sort of "ad" object at desired locations within your data model.
Then update cellForRowAt to look at the object in the data model for the given index path. If it's an "ad" object, create and setup an "ad" cell. Otherwise create and setup an appropriate data cell as you do now.

swift code error

import UIKit
class ViewController: UITableViewController {
var animals = [Animal]()
override func viewDidLoad() {
super.viewDidLoad()
self.animals = [Animal(name: "개"),Animal(name: "강아지"),Animal(name: "고양이"),Animal(name: "멍멍이"),Animal(name: "물어")]
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
var animal = Animal.self
animal = animals[indexPath.row] //1
cell.textLabel?.text = animal.name //2
return cell //3
}
}
I am getting the following errors:
error is cannot assign value of type 'Animal' to type 'Animal Type'
error is instance member 'name' cannot be used on type 'Animal
unexpected non-void return value in void function
In your code above, "didSelectRowAtIndexPath" is called after you select certain cell, and it does not return anything. Instead, if you want to display the cell, use "cellForRowAtIndexPath".
class Animal {
var name: String
init(name: String) {
self.name = name
}
}
class ViewController: UIViewController {
var animals = [Animal]()
override func viewDidLoad() {
super.viewDidLoad()
self.animals = [Animal(name: "개"),Animal(name: "강아지"),Animal(name: "고양이"),Animal(name: "멍멍이"),Animal(name: "물어")]
}
}
extension ViewController: UITableViewDataSource {
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
// This would works on your table view
var animal = animals[indexPath.row]
cell.textLabel?.text = animal.name
return cell
}
}

Table View UI error: Swift

On the last question I asked about the code error in my animal table view project, now I finished the initial coding, but my UI turned really strange. It is missing the first letter of each animal name and the table view prototype cell.
For example, amel should be camel and hinoceros should be rhinoceros.
Is this a bug from the code here?
import UIKit
class AnimalTableViewController: UITableViewController {
var animalsDict = [String: [String]] ()
var animalSelectionTitles = [String] ()
let animals = ["Bear", "Black Swan", "Buffalo", "Camel", "Cockatoo", "Dog", "Donkey", "Emu", "Giraffe", "Greater Rhea", "Hippopotamus", "Horse", "Koala", "Lion", "Llama", "Manatus", "Meerkat", "Panda", "Peacock", "Pig", "Platypus", "Polar Bear", "Rhinoceros", "Seagull", "Tasmania Devil", "Whale", "Whale Shark", "Wombat"]
func createAnimalDict() {
for animal in animals {
let animalKey = animal.substringFromIndex(advance(animal.startIndex, 1))
if var animalValues = animalsDict[animalKey] {
animalValues.append(animal)
animalsDict[animalKey] = animalValues
} else {
animalsDict[animalKey] = [animal]
}
}
animalSelectionTitles = [String] (animalsDict.keys)
animalSelectionTitles.sort({ $0 < $1})
animalSelectionTitles.sort( { (s1:String, s2:String) -> Bool in
return s1 < s2
})
}
override func viewDidLoad() {
super.viewDidLoad()
createAnimalDict()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// Return the number of sections.
return animalSelectionTitles.count
}
override func tableView(tableView: UITableView, titleForHeaderInSection section:Int) -> String? {
// Return the number of rows in the section.
return animalSelectionTitles[section]
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
let animalKey = animalSelectionTitles[indexPath.section]
if let animalValues = animalsDict[animalKey] {
cell.textLabel?.text = animalValues[indexPath.row]
let imageFileName = animalValues[indexPath.row].lowercaseString.stringByReplacingOccurrencesOfString("", withString: "_", options: nil, range: nil)
cell.imageView?.image = UIImage(named:imageFileName)
}
return cell
}
}
So far I can say the error is in your createAnimalDict() method. In the line
let animalKey = animal.substringFromIndex(advance(animal.startIndex, 1))
exchange the second parameter in advance to 0 so it be:
let animalKey = animal.substringFromIndex(advance(animal.startIndex, 0))
In fact I don't really know what you are trying to do.
In this method:
override func tableView(tableView: UITableView, titleForHeaderInSection section:Int) -> String? {
// Return the number of rows in the section. (THIS COMMENT IS INCORRECT)
return animalSelectionTitles[section]
}
You are returning the title for each section. However your animalSelectionTitles[index] contains the animal name without the first letter due to its construction in createAnimalDict
Use the animal array instead for providing the full animal names:
override func tableView(tableView: UITableView, titleForHeaderInSection section:Int) -> String? {
return animals[section]
}
However note that due to the removal of the first letter you might have two animals mapping to the same key so if not necessary use the entire animal name as the key.