Swift using guard and fatal error specified in an function - ios

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 :)

Related

How do you make a UITableView with Sections using NSFetchedResultsController?

So we have to use CoreData and we have to use a UITableView, now I'm asking how does that work with a NSFetchedResultsController? We have something already set up for other basic sectioned views where we dont need a fetchresultscontroller and that was pretty easy, but this seems a little trickier. There is a little support out there for our old pal Objective-C that has been around for years but we are looking for a Swift solution. Swift 5 at the time of this post.
Have already implemented the plain list (which is fairly well documented) and now we just want to add some sections...
So we have a UITableView and a NSFetchedResultsController and we get a nice list of Entities. Now we want to add a few sections into the tableview.
No Problem so we update the sortDescriptors to include a section:
fetchRequest.sortDescriptors =
[NSSortDescriptor(key: "sectionLabel", ascending: false),
NSSortDescriptor(key: "unixTimestamp", ascending: false)]
and the entity to include the sectionLabel, this part is a bit more custom depending on what kind of sections you want.
Then we add a few section functions :
func numberOfSections(in tableView: UITableView) -> Int {
return self.fetchController?.sections?.count ?? 0
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard AllTheThings( fetchController -> sections -> entity -> cell )
if sections.count > 1 {
cell.configureCell(value: entity.sectionLabel ?? "myLabel")
return cell.contentView
} else { return UIView() }
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 20.0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let sections = fetchController?.sections else { return 0 }
return sections[section].numberOfObjects
}
So things are starting to shape up but theres still some work needed on the cellForRowAt indexPath and other standard cell functions. So in order to get an entity for the index to use in those we create a cool function:
func returnEntity(indexPath: IndexPath) -> MyEntity? {
guard let sections = fetchController.sections else {
return nil
}
guard let objects = sections[indexPath.section].objects else {
return nil
}
if objects.isEmpty {
return nil
}
guard let entity = objects[indexPath.row] as? MyEntity else {
return nil
}
return entity
}
Left the guards in this time ;)
Now all you have to do to get your entity is use returnEntity(indexPath: indexPath)

Best way to populate rows in section in UITableView?

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)

fatal error: Index out of range

I want to show 25 of the songs I have in my library. This is my code:
var allSongsArray: [MPMediaItem] = []
let songsQuery = MPMediaQuery.songsQuery()
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 25 //allSongsArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell")
let items = allSongsArray[indexPath.row]
cell?.textLabel?.text = items.title
cell?.detailTextLabel?.text = items.artist
cell?.imageView?.image = items.artwork?.imageWithSize(imageSize)
return cell!
}
When I run this, it crashes, because:
fatal error: Index out of range
I tried to change the numberOfRowsInSection to allSongsArray.count, but it ends up with the same error.
You should return allSongsArray.count and to avoid being returned empty cells use yourTableView.reloadData after you fill your allSongsArray.
When you first create the array it is empty. Hence, it will give you out of bound error.
Try to return the count of the songs array instead.
1st you need to get the data into the array and then update the table view.
Here is a sample code:
#IBAction private func refresh(sender: UIRefreshControl?) {
if myArray.count > 0 {
self.tableView.reloadData()
sender?.endRefreshing()
} else {
sender?.endRefreshing()
}
}
In case your app crashes in
let items = allSongsArray[indexPath.row]
as you check if the allSongsArray.count, u can safe guard it by making sure that the [indexPath.row] doesn't exceed your array count. so u can write a simple if condition as;
if allSongsArray.count > 0 && indexPath.row < allSongsArray.count {
//Do the needful
}
Please return the actual array count instead of static
return allsongsarray.count
For folks looking for a solution to swiping on a filtered list, you need to provide the section as well as the row or xcode throws this error.
func swipe(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let myRow = filteredList[indexPath.section].items[indexPath.row]
}
not
func swipe(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let myRow = filteredList[indexPath.row]
}

UITableView with different optional sections?

I am looking for a "good" way to solve some special requirements:
I have an UITableView with different sections, for example:
Base Data
About me
Interests
Images
Base Data contains always values (but there is still an variable row count) - and all other "Categories" could contain rows, or still could be empty. If there is no data, the category should be not shown.
No my first idea to solve that is:
Create all possible categories (but that could be 20 or more) - and do something like that:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var count:Int = 0
switch (section) {
case 0:
count = baseHeaders.count
case 1:
if(mapItem.courses?.count > 0) {
count = mapItem.courses!.count
}
break;
default:
count = 0
}
return count
}
And ill check also with: titleForHeaderInSection if the count is null, and return then no header for the section.
BUT: is that a good way? My concern is about creating 20 sections and just 2 are used. Is there another, better way? Can i create sections manually? If yes, should i? So that only the base category is visible, and append everything else if there is data available.
This looks like my way of approaching such problems. I'm using enums (Obj-C & especially Swift) to handle and identify my Sections and I always return the full amount of potential sections:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return FormSection.count // enum function
}
In func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int, however, I switch the unused sections off by returning 0 rows.
The benefit I saw after struggling with your type of dynamic tables was that all sections are always at the same index which made cell management relatively easy:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let section:FormSection = FormSection(rawValue:indexPath.section)!
switch section {
case .Header:
//…
default:
//…
}
}
The same goes for section headers/footers:
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
switch section {
case FormSection.Header.rawValue:
return nil
case FormSection.RoomSetup.rawValue where foo == false:
return nil
default:
// return header with title = FormSection(rawValue: section)?.headerTitle()
// Swift enums ftw ;)
}
And the number of rows is calculated/fetched at runtime:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section:FormSection = FormSection(rawValue:section)!
switch section {
case .Section1:
return fooExpanded ? (numberOfFoo) : 0
case .Section2:
return model.barCount()
default:
return 1
}
}

Return no cell based on nil variable

This is a bit of a continuation of this question, but basically I am trying to figure out how I can return no cell if the result of a function is nil.
This is my code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("rideCell", forIndexPath: indexPath) as! RideCell
var ride = DataManager.sharedInstance.getRideByName(favouritesArray[indexPath.row] as! String)
println(ride)
if ride != nil {
cell.rideNameLabel.text = ride!.name
var dateFormat = NSDateFormatter()
dateFormat.dateFormat = "h:mm a"
cell.updatedLabel.text = dateFormat.stringFromDate(ride!.updated!)
if ride!.waitTime! == "Closed" {
cell.waitTimeLabel.text = ride!.waitTime!
} else {
cell.waitTimeLabel.text = "\(ride!.waitTime!)m"
}
}
return cell
}
So at the moment everything works, however wherever ride does equal nil, I just get the prototype cell returned, whereas I would like it to return nothing.
I have tried hiding or changing the height of these cells to nil, but it just seems like messy solution. Anyone know a better one?
Thanks.
determine the number of rows by implementing tableView(_ tableView: UITableView, numberOfRowsInSection section: Int)
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) {
if section == 0 {
return arrayName.count
}
return 0
}
I figured out a solution. As a few people mentioned, I had to figure out what to display before dealing with cellForRowAtIndexPath.
Basically I added some code to figure out which favourites could be found in the array and put it in viewWillAppear.
for index in 0...favouritesArray.count - 1 {
var ride = DataManager.sharedInstance.getRideByName(favouritesArray[index] as! String)
if ride != nil {
favouritesFound.append(ride!.name!)
println(favouritesFound)
}
}
Works perfectly now! Thanks everyone.
It is assumed that you always know how many valid rows do you have and you publish this number by implementing UITableViewDataSource's tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int method. So if at some moment number of rows changes you should call UITableView.reloadData not to try to hide excess rows.
To get an array of valid rides try something like:
var newArray:[String] = []
for str in favouritesArray where DataManager.sharedInstance.getRideByName(str!) != nil
{
newArray += [str]
}

Resources