I am building a very simple timetable iOS app to test out Swift and Parse.com as a backend.
I have an object type in my Parse database called "Class" (for school classes). These are simple objects that contain a name and dayOfWeek (Monday - Friday).
I want to display these similar to the iOS Calendar app 'list view', with a section of each day: Monday, Tuesday, Wednesday, Thursday and Friday (not actual dates like the Calendar app, just the week day name).
Then I want to display under each day, only the PFObjects returned from a query with same dayOfWeek as the relevant section.
I tried achieving this with a PFQueryTableViewController, but got weird issues with no section names and random empty cells throughout the table.
I believe that I need to take the following approach, but so help would be highly appreciated.
Retrieve a PFQuery of all my Class objects
Set numberOfSectionsInTable to 7 (one for each day)
Set titleForSection to appropriate names
In cellForRowAtIndexPath, cycle through returned Class objects, allocating each to the correct section based on dayOfWeek. Not sure how to do this part.
I have searched around and found that there seems to be no clear tutorials or documentation on how to show PFObjects from a PFQuery in multiple sections, sorted by one of the values of the object.
Any help would be appreciated. I believe this will help a lot of other people too.
You could create dynamic sections with a 2D array containing the name of the current section and an array to hold the the items.
You can then simply call
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return sectionItems.items[section].count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return sectionItems.items[section].count
}
sectionItems can be created using this class with takes a string and an array as a parameter
class SectionItems:NSObject{
var sections:[String] = []
var items:[[String]] = []
func addSection(section: String, item:[String]){
sections = sections + [section]
items = items + [item]
}
}
You can the call the function by using
self.sectionItems.addSection("Intro", item: self.intro)
Related
The main idea is to have all sections in an array or you may suggest other solution to build a table. I had this code below to prepare data for a table:
enum ExerciseSection {
case empty
case exerciseGroup(group: ExerciseGroup)
}
struct ExerciseGroup {
var muscleGroupName: String
var exercises: [ExerciseEntity]
var selectedExercisesIndexes: [Int]
}
As you see using this ExerciseSection enum I can simple check if the section is static empty or it should display some muscle group name. Also Group contains exercises. So I can simple a build needed cell.
So I prepare data for my table by creating an array of ExerciseSection.
In this concert example my empty cell is a cell which redirect me to the other screen.
Looks like this:
[empty cell for section 0, group for section 1, group for section 2... and etc]
Now I changed mind of preparing my own sections and instead I started using CoreStore.monitorSectionedList
typealias ListEntityType = ExerciseEntity
let monitor = CoreStore.monitorSectionedList(
From<ListEntityType>()
.sectionBy(#keyPath(ListEntityType.muscle.name)) { (sectionName) -> String? in
"\(String(describing: sectionName)) years old"
}
.orderBy(.ascending(\.name))
)
So now my data is grouped automatically by relationships muscle name.
I can simple access instance of monitor and see how many sections it has and how many rows it has for appropriate section. Pretty awesome!
But my question now how can I combine monitor object which has all needed info about grouped objects and about groups with my static cells.
In my example above I have the firs element empty cell for section 0 but monitor already has section 0 as well.
So I need to have a hack to add 1 + which I really don't like as this is a magical number and some day it will surprise me.
func numberOfSections(in tableView: UITableView) -> Int {
return 1 + (monitor.numberOfSections() ?? 0) // My static section + monitor sections
}
In the previous time I just had array of all my sections [ExerciseSection] so there is no needs to control code via 1 +
I need to glue somehow my static section info and monitor.sections
You may never used CoreStore before, so never mind you can just think about monitor object as an object that has some groups to represent sections and these groups has items to represent rows. So I just need to combine it.
In my case you can simple see that the static cell is a first one item in the list but I am looking for flexible solution I even can't imagine how to show static cell at the middle of list for example.
Maybe as a solution I can loop through monitor objects and create my enum from it. Not sure.
Hmm... The "easiest" way would be to have a computed property sections or similar.
Something along the lines of
var sections: [ExerciseSection] {
return [.empty] + monitor.sections
}
But if the monitor doesn't have a direct way to get sections, then maybe the best way would be to simply have a list of "pre-sections".
let presections: [ExerciseSection] = [.empty]
func numberOfSections(in tableView: UITableView) -> Int {
return presections.count + (monitor.numberOfSections() ?? 0) // My static section + monitor sections
}
You could add a couple of functions to help you such as
func section(at indexPath: IndexPath) -> ExerciseSection {
guard indexPath.section >= presections.count else {
return presections[indexPath.section]
}
return monitor.section(at: indexPath.section - presections.count)
}
You do mention looping through the monitor objects, and this can be nice for smaller datasets. The downside is that you suddenly store data in memory. I don't know how the monitor works.
With Realm I've done this but only stored the id and maybe some simple data for each row.
I have an array of Dates which I'd like to filter, depending on a date selected by a user. However, since each object has also time, I am having a hard time filtering correctly.
In my table view, I would like to filter this array of items by their date parameter without it comparing the time. Aka:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.filter({$0.date == selectedDate}).count
//count is always 0 because time is not exactly the same in any instance
}
Any ideas on how I can do this?
Try using this function of the system calendar
Calendar.current.isDate($0.date, inSameDayAs: selectedDate)
Here is the apple documentation for this function in case you have an additional clarifying questions
https://developer.apple.com/documentation/foundation/calendar/2292885-isdate
How you doing fellow developers?
I’m relatively new to iOS development and still struggling to implement what in other languages I do relatively easy.
I’m building an Events App. On my App, the user can choose which categories of events he wants to see everytime he runs the App.
Sometimes according to some filters like [Date], certain categories may not have results to be shown. e.g.: He picked categories A, D, F, G and M. But for today, only D, F and M satisfies the criteria i.e. have Events to show.
So, the user preferences gives an Array of Categories (Sections). Each category should only be shown if the array of events (items in Section) for each category has at least one item.
The problem is, In my UICollectionView I want to implement the numberOfSections and numberOfItemsInSection methods according to the above, with header titles, footer, etc.
Mentally I can go through the necessary logic to achieve that, like, return the count considering only those categories with at least one item. But I’m having troubles to translate it to swift code.
How should I approach it? Can somebody share a code snippet implementing that?
Thanks in advance.
You can filter your Array of categories and then use the result as the source for the datasource functions of your collectionView.
Sample code:
// replace these structs by your actual models
struct Event {
let id: Int
}
struct Category {
let events: [Event]
}
// this is your Array of categories with 3 elements
let categories = [Category(events: [Event(id: 0)]), Category(events: [Event(id: 1)]), Category(events: [])]
// after filtering out the categories without events, the resulting Array has only 2 elements
let filteredCategories = categories.filter { $0.events.count > 0 }
Then you can implement the datasource functions for the collectionView like this:
func numberOfSections(in collectionView: UICollectionView) -> Int {
return filteredCategories.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let category = filteredCategories[section]
return category.events.count
}
In my CoreData, I have a Person entity, and each Person can have multiple (to Many) Statement entities. The statement entity has an attribute called amountOwed which is a decimal amount. Right now my idea is to loop over all the amounts and add them up, if they of a positive amount add them to the positive array and if they are negative amount add them to that array. Then use that array to figure out how many cells each sections needs to display.
I created a fetchedResultsController and I am trying to use that for the for loop
for i in 0..<fetchedResultsController.fetchedObjects!.count {
let person = fetchedResultsController.fetchedObjects?[i]
let amountTotal = person?.value(forKeyPath: "statement.#sum.amountOwed") as? Decimal
if(amountTotal! <= Decimal(0) )
{
postiveCellNumber += 1
print("\(postiveCellNumber) postive number count")
}
else{
negativeCellNumber += 1
print("\(negativeCellNumber) negative number count")
}
}
Then, I'm trying to use those arrays in the numberOfRowsInSection function like so:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch(section) {
case 0:
return postiveCellNumber
case 1:
return negativeCellNumber
default :return 0
}
}
I don't think my for loop is looping properly as I get an error that says
no object at index 2 in section at index 0.
How about using two different queries, one for the positive values and one for the negative ones, with two different fetched results controllers? Then you can let the FRC do the iterating and counting for you.
You won't be able to use the FRCs to manage sections. You'll have to do that yourself. Specify nil for the sectionNameKeyPath. Then you want something like
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return positiveResultsController.fetchedObjects?.count ?? 0
}
else {
return negativeResultsController.fetchedObjects?.count ?? 0
}
}
or maybe
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
let sectionInfo = positiveResultsController.sections![section]
return sectionInfo.numberOfObjects
}
else {
let sectionInfo = negativeResultsController.sections![section]
return sectionInfo.numberOfObjects
}
}
with similar logic for tableView(_:cellForRowAt:).
It's very difficult to identify the specific cause of the error, without seeing more of how your FRC and tableView are constructed. But this bit looks suspicious:
if(amountTotal! <= Decimal(0) )
{
postiveCellNumber += 1
print("\(postiveCellNumber) postive number count")
}
Surely the if condition is back to front: you should increment the positiveCellNumber if amountTotal! >= Decimal(0)?
But that aside, even if you successfully calculate the counts, you will then face the issue of working out which of the FRC's fetchedObjects should appear in each row of each section:
You could do this "on the fly" in cellForRowAt, but that will involve iterating through the fetchedObjects again, to determine which appear in which section, which is clumsy and inefficient.
You could separate the fetchedObjects out into two separate arrays as a one-off step, once the FRC has done its performFetch. But you then need pretty ugly FRC delegate methods to update the two arrays whenever the FRC's fetchedObjects array is updated.
You could configure your FRC to automatically assign its fetched objects to the correct section, by specifying a sectionNameKeyPath and associated sort descriptors for the underlying fetch. The problem here is that it is not possible to sort the fetch using a calculated figure. If you want to pursue this route, you will need to add a totalAmountOwed attribute to your Person entity, and to ensure it is updated whenever the related Statements change.
Alternatively, you could follow #HalMueller's suggestion, and use two separate FRCs. You would use complementary predicates for the underlying fetches, one to get only those Persons with positive amount owed, the other to get those with negative amount owed. You can then use the fetchedObjects array for one FRC to populate section 0, and the other FRC to populate section 1. Overall, I think this is the solution I would recommend.
Here is my viewDidLoad method -
var query = PFUser.query()
query.findObjectsInBackgroundWithBlock({ ( objects : [AnyObject]! , error: NSError! ) -> Void in
//self.users.removeAll(keepCapacity: true)
for object in objects {
var user:PFUser = object as PFUser
println(user.username)
self.users.append(user.username)
}
self.tableView.reloadData()
})
println( " Count \(users.count) ")
The count of users gets printed before the usernames in the users which makes me believe that it is taking time to fetch the users from the database. And for that reason my tableView never gets updated, the code for which looks like this -
var cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell
cell.textLabel?.text = users[indexPath.row]
return cell
Gives me an 'Array index out of range error' because the number of rows in my table is three while my dictionary is empty.
Couldn't find any particular solution on swift. Any suggestions?
EDIT : Forgot to mention that the users do get printed but after a long time (even after the count of the users which are being printed after the usernames are)
Just for the information, count is always printed as 1.
The output is something like this -
Count 1
genaks
genaks1427
genaks14271
adu
afd
I shifted my the code in my viewDidLoad function to the viewDidAppear function and it works just about the way I wanted it to work :)
If you can, do the findObjectsInBackgroundWithBlock query inside the cellForRowAtIndexPath method. Check this tutorial: https://www.youtube.com/watch?v=Q6qcrO8uNzU he does the same in video.
When you said "the number of rows in my table is three" you inferred that in numberOfRowsInSection you returned 3.
If I'm right, I believe that's the problem.
You have to do it like this
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
This way the numberOfRowsInSection will never be different from the intended amount.