I've googled for about 2 days about my problem already. But I couldn't find any solution that matches with my problem. My problem is I want to make a custom SectionIndexTitle for UITableView like an image below. Please recommend me any library or concept to do it. Thank you.
If you're trying to create an alphabetic index there is method in UITableViewDelegate that supports it out of the box.
You'll need to define your sections and then use UILocalizedIndexedCollation to index them.
let collation = UILocalizedIndexedCollation.currentCollation()
as UILocalizedIndexedCollation
// table sections
var sections: [Section] {
if self._sections != nil {
return self._sections!
}
// create objects from your datasource
var objects: [Object] = names.map { objectAttribute in
var object = Object(objectAttribute: objectAttribute)
object.section = self.collation.sectionForObject(object, collationStringSelector: objectAttribute)
return object
}
// create empty sections
var sections = [Section]()
for i in 0..<self.collation.sectionIndexTitles.count {
sections.append(Section())
}
override func sectionIndexTitlesForTableView(tableView: UITableView)
-> [AnyObject] {
return self.collation.sectionIndexTitles
}
override func tableView(tableView: UITableView,
sectionForSectionIndexTitle title: String,
atIndex index: Int)
-> Int {
return self.collation.sectionForSectionIndexTitleAtIndex(index)
}
To read more about UILocalizedIndexedCollation
Related
I am making an app in which I need this thing in one of the screens.
I have used the tableview with sections as shown in the code below
var sections = ["Adventure type"]
var categoriesList = [String]()
var items: [[String]] = []
override func viewDidLoad() {
super.viewDidLoad()
categoryTableView.delegate = self
categoryTableView.dataSource = self
Client.DataService?.getCategories(success: getCategorySuccess(list: ), error: getCategoryError(error: ))
}
func getCategorySuccess(list: [String])
{
categoriesList = list
let count = list.count
var prevInitial: Character? = nil
for categoryName in list {
let initial = categoryName.first
if initial != prevInitial { // We're starting a new letter
items.append([])
prevInitial = initial
}
items[items.endIndex - 1].append(categoryName)
}
for i in 0 ..< count
{
var tempItem = items[i]
let tempSubItem = tempItem[0]
let char = "\(tempSubItem.first)"
sections.append(char)
}
}
func getCategoryError(error: CError)
{
print(error.message)
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.sections[section]
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = categoryTableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath)
cell.textLabel?.text = self.items[indexPath.section][indexPath.row]
return cell
}
But it is producing runtime errors on return self.items[section].count
The reason for this error is because I am loading data (items array) is from server and then populating sections array after it. At the time when tableview gets generated, both the sections and items array is empty. That is why error occurs.
I am new to iOS and not getting grip over how to adjust data in sections of tableview.
Can someone suggest me a better way to do this?
What should be number of rows in section when I have no idea how much items server call will return?
Also i want to cover the case when server call fails and no data is returned. Would hiding the tableview (and showing error message) be enough?
Any help would be much appreciated.
See if this works: Make your data source an optional:
var items: [[String]]?
And instantiate it inside your getCategorySuccess and fill it with values. Afterwards call categoryTableView.reloadData() to reload your table view.
You can add a null check for your rows like this:
return self.items?[section].count ?? 0
This returns 0 as a default. Same goes for number of sections:
return self.items?.count ?? 0
In case the call fails I would show an error message using UIAlertController.
Your comment is incorrect: "At the time when tableview gets generated, both the sections and items array is empty. That is why error occurs."
According to your code, sections is initialized with one entry:
var sections = ["Adventure type"]
This is why your app crashes. You tell the tableview you have one section, but when it tries to find the items for that section, it crashes because items is empty.
Try initializing sections to an empty array:
var sections = [String]()
Already things should be better. Your app should not crash, although your table will be empty.
Now, at the end of getCategorySuccess, you need to reload your table to reflect the data retrieved by your service. Presumably, this is an async callback, so you will need to dispatch to the main queue to do so. This should work:
DispatchQueue.main.async {
self.categoryTableView.reloadData()
}
I've got a simple UITable implementation and an array which is called fruits. Taken out of this example. The only difference is that I am using a UIViewController for the UITable insted of a UITableViewController, but this should be irrelevant for now.
class MainViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var fruits : [String] = Array()
let thread : GetDataForFruitsArrayThread
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fruits.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)
cell.textLabel?.text = fruits[indexPath.row]
return cell
}
}
Now I have got a viewDidLoad function in this class also. In the function I am calling a thread which is getting the array as a parameter. The thread is filling up the array with new values asynchronously. Means I don't know if there will be any values or when do the value appear and are set into the array. So you can imagine what I want to ask on this point: How do I signal the UITableView that I have changed the content of the array once the view has loaded the array into the UITable. In c# there is the NotifyAll() function which notfies all the Listener to an attribute. Is there an easy and performant way to do this in swift, optionally without importing any non-included libraries?
override func viewDidLoad() {
super.viewDidLoad()
self.thread = GetDataForFruitsArrayThread(array: &fruits)
self.thread.StartAsynchronously()
}
EDIT - adding GetDataForFruitsArrayThread
class GetDataForFruitsArrayThread : MyThread {
var array : [String]
init(array: inout [String] ) {
self.array = array
}
override func main() {
// init socket
while(true){
_ = withUnsafeMutablePointer(to: &rcvaddr){
$0.withMemoryRebound(to: sockaddr.self, capacity: 1){
recvfrom(m_socket, &pData, bufferlength, 0, UnsafeMutablePointer($0),
&socketAddressLength)
}
}
var fruit = String(cString:inet_ntoa(rcvaddr.sin_addr), encoding: .ascii)
self.array.append(fruit)
}
}
}
EDIT - 21.11.2017:
Following Sandeep Bhandari's answer I changed the following code in the ViewController:
#IBOutlet weak var tableView1: UITableView!
var fruits : [String] = Array() {
didSet {
self.tableView1.reloadData()
}
}
PROBLEM: I am setting a breakpoint at array.append(fruit) in the GetDataForFruitsArrayThread-class. After that there is a breakpoint in the didSet method. But this breakpoint does not get called after the append()-call! NOTE: The parameter on the init function of the GetDataForFruitsArrayThread-class is an in-out parameter!
UPDATE: I found out that if I do the array.append(fruit)-call on the GetDataForFruitsArrayThread.array in the GetDataForFruitsArrayThread-thread, the Fruits-array in ViewController-class does not get changed, therefore didSet did not get called. How can I change this behaviour?
Because you said you are running a endless process to keep updating your array and you want the UI update once the array changes you can use the below solution.
var fruits : [String] = Array() {
didSet {
self.tableView.reloadData()
}
}
What am I doing ? Simple added a setter to array and whenever value changes I reload the tableView.
EDIT:
The above solution will work only if you modify the inout fruits array you pass to GetDataForFruitsArrayThread
self.thread = GetDataForFruitsArrayThread(array: &fruits)
So don't create one more array property in GetDataForFruitsArrayThread rather directly modify the array passed to GetDataForFruitsArrayThread.
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)
I have a UITableView with sectionIndexTitles. Here's my data source :
let sSectionTitles = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","#"]
var sectionTitles = [String]()
func sectionIndexTitlesForTableView(tableView: UITableView) -> [String]? {
return sSectionTitles
}
func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
var section = 0
if let selectedSection = sectionTitles.indexOf(title) {
section = selectedSection
} else {
section = index
}
return section
}
The variable sectionTitles is a similar array to sSectionTitles except that it only contains section indexes that are valid. For example, if I have no Contact with their name starting with the letter D, then "D" won't be in sectionTitles.
I'm trying to replicate the behavior in the Contact application :
If the user clicks on the Index title "D" and if there is at least one contact in the B section, then scroll to this section.
Else, scroll to the previous section available. (In this example, if there are no contacts for the B and C letter then scroll to A)
I've been stuck of this for many hours I still don't know how I could apply this logic. I thought about using a recursive function but I didn't manage to pull this off. Does someone has any lead on how this could be achieved?
Thanks.
I think you can do it by recursion. Use another helper function to retrieve the appropriate index and call it from tableview data source function. Example,
func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
var section = getSectionIndex(title)
return section
}
//recursive function to get section index
func getSectionIndex(title: String) -> Int {
let tmpIndex = sectionTitles.indexOf(title)
let mainIndex = sSectionTitles.indexOf(title)
if mainIndex == 0 {
return 0
}
if tmpIndex == nil {
let newTitle = sSectionTitles[mainIndex!-1]
return getSectionIndex(newTitle)
}
return tmpIndex!
}
I am completely new to swift and I am pretty find it difficult to find that its quite different than obj.C
I have a difficulty while populating the table view.
my coding to populate goes as follows -
class DetailTableViewController: UITableViewController
{
var items = []
override func viewDidLoad() {
super.viewDidLoad()
items=["dodnf","dgfd"]
Item()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier("cellreuse", forIndexPath: indexPath)
cell.textLabel?.text = items[indexPath.item]
return cell
}
When I am writing the following code cell.textLabel?.text = items[indexPath.item] I am getting an error message as- AnyObject is not convertible to String
So what is my error and why is it so?
Just add the type when declaring the data source array:
var items : [String] = []
That's all. In cellForRowAtIndexPath the compiler can infer the type.
However the proper syntax is supposed to be
cell.textLabel?.text = items[indexPath.row]
The problem is that you have declared items as an array without specifying the type of the elements (var items = []). Therefore, when you try to get an element from the array, the compiler errors because it cannot guarantee the type of the element is what you are expecting.
You need to specify the type of the items in the array. You can do this in either one of the two possible stages:
when you declare the array (preferred to leverage Swift's type safety):
var items = [String]() // or its equivalent: var items : [String] = []
// Alternatively, if you know it at the time of declaration you can just do the following and let
// Swift's type inference do its work
var items = ["dodnf","dgfd"]
OR when you get an element from the array:
cell.textLabel?.text = items[indexPath.item] as? String
You can learn more about Collection Types in Swift in The Swift Programming Language.