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.
Related
Background
In my app, I store a bunch of object IDs. I use these IDs to make batch API calls. The API limits each call to 10 ID numbers. This data is rendered on a UITableView. The user can add and delete objects, which adds or removes the object ID from the database.
I’m using a Firestore database to store the object IDs on my end.
Current Implementation
Here’s what I’ve implemented so far, but it crashes the app when add & deleting objects. I haven’t been able to work out how to properly handle these cases & whether this is the right pattern to do something like this.
Get object IDs to be used for making API calls
var objectIds: [String] = []
var chunkedObjectIds: [[String]] = []
var objects: [Array] = []
var offset: Int = 0
override func viewDidLoad() {
super.viewDidload()
getObjectIds()
}
func getObjectIds() {
// get objects IDs and store then in objectIds from the Firestore database
// setup the .addSnapshotLister so the query is triggered whenever there is a change in the data on Firestore for the collection
return chunkedObjectIds
// when finished, get the first 10 objects from the 3rd party API
fetchObjects()
}
Take object Ids array, split into array of arrays (lots of 10) & Make the API call for the first 10
func fetchObjects() {
// split objectIds array in array of arrays, in lots of 10
// chunkedObjectIds is set here
// request the objects for the first 10 ID numbers
Alamofire.request(… parameter with first 10 object ids …) (objects) in {
// save objects
// increment the offset
offset += 1
}
}
Render the data on the UITableView cells
Use the following method to load more data from the 3rd party API:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastRow = objects.count
var parameters = [String: Any]()
if indexPath.row == lastRow {
if !(offset == self.chunkedObjectIds.count) {
// process the next batch from the array
parameters["id-numbers"] = self.chunkedObjectIds[offset].map{String($0)}.joined(separator: ",")
Alamofire.request(… paramaters: parameters) { (objects) in
for item in 0..<objects.count {
let indexPath = IndexPath(row: item + self.objects.count, section: 0)
self.paths.append(indexPath)
}
self.objects.append(contentsOf: objects)
self.tableView.beginUpdates()
self.tableView.insertRows(at: self.paths, with: .automatic)
self.tableView.endUpdates()
self.paths.removeAll()
self.offset += 1
}
}
}
}
Adding or deleting objects:
The object ID is added or deleted from the Firestore database
The objectIds, chunkedObjectIds, offset and objects are cleared
The listener triggers a read of the data and the process repeats
The Issue & Question
This works well to load initial data. But duplication occurs when adding (and sometimes crashing). When deleting the app will crash because of out of range exceptions.
Is this the correct pattern to use in the first place? If so, what am I missing to handle cases after the first load, specifically the addition and deletion of new object IDs.
Edit
I have changed the implementation based on feedback in the comments. So now, the process is like this:
Setup listener to get data from Firestore
Loop through the object ids from Firestore and while the counter is < 10 or we reach object.count - Now I save the next offset and the next time it triggers this method, I initiate a loop from the next offset with the same while conditions
Fetch the objects from the 3rd party API
I kept using willDisplay cell method to trigger more data to load - it seemed to work more reliably than scrollDidEnd method.
So now the app doesn't crash anymore. There are some issues with the firestore listener, but I'll post that as a separate question.
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
}
I want to show some images on UITableViewCell. However I got an error below
fatal error: Index out of range. The problem is that closure does not run in the main thread probably. How can I solve this issue?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PickupTableViewCell", for: indexPath) as! PickupTableViewCell
APIManager.getAnotherArticle{ (articles: Array<Article>?) in
for info in articles! {
self.authorArray.append(info.author)
self.descriptionArray.append(info.description)
if info.publishedAt != nil {
self.publishedAtArray.append(info.publishedAt)
}
self.titleArray.append(info.title)
self.urlArray.append(info.url)
self.urlToImageArray.append(info.urlToImage)
print(self.authorArray)
}
}
let program = urlToImageArray[indexPath.row] //index out of range
let urlToImage = NSURL(string: program)
cell.pickupImageView.sd_setImage(with: urlToImage as URL!)
return cell
}
Wrap anything you want to run on the main queue in DispatchQueue.main.async{ ... }.
That said, your current approach likely won't work. This method gets called a lot. While the user is scrolling, this method gets called every time a cell is about to come on the screen (in iOS 10, sometimes a bit before it'll come on the screen). Cells are often recycled, and you're appending data to the titleArray and other arrays every time a cell is requested (they may not be in order; they might have already been fetched; this array isn't going to wind up in the right order).
You need to move all your data about a cell into a model object and out of the view controller. There shouldn't be a titleArray and an urlArray, etc. There should just be an Article, and the Article should take care of fetching itself and updating its properties. And the job of this method is to fetch the correct Article from your cache, or create a new one if needed, and assign it to an ArticleCell. The ArticleCell should watch the Article and update itself any time the Article changes (i.e. when the fetch completes). Almost no work should happen directly in this method since it gets called so often, and in possibly random orders.
The common way to build this kind of thing is with a simple model object (often a reference type so it can be observed; there are many other approaches that allow a struct, but they're a little more advanced so we'll keep this simple):
class Article {
var author: String
var description: String
var publishedAt: Date
var title: String
var url: URL
var image: UIImage
func refresh() {
// fetch data from server and replace all the placeholder data
}
}
Then there's some kind of Model that vends these:
class Model {
func article(at index: Int) -> Article {
if let article = lookupArticleInCache(at: index) {
return article
}
let article = createAndCachePlaceholderArticle(at: index)
article.refresh()
}
}
And then your code looks like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PickupTableViewCell", for: indexPath) as! PickupTableViewCell
cell.article = sharedModel.article(at: indexPath.row)
return cell
}
You can use KVO or Swift Observables or an ArticleDelegate protocol to let the cell observe the Article. When the Article updates, the cell updates itself.
Again, there are many ways to approach this. You could have a "PlaceHolderArticle" that all the cells share and when the real Article comes in, the cell replaces the whole thing (so that Articles are immutable rather than self-updating). You could use the more generic approaches described by Swift Talk. There are lots of ways. But the key is that there is this model that updates itself, independent of any particular UI, and a UI (views, view controllers) that watch the model and display what it holds.
If you want much, much more on this topic, search for "Massive View Controller." That's the common name for the anti-pattern you're currently using. There are lots of ways to fight that problem, so don't assume that any particular article you read on it is "the right way" (people have come up with some very elaborate, and over-elaborate, solutions). But all of them are based on separating the model from the UI.
APIManager.getAnotherArticle{ (articles: Array<Article>?) in
for info in articles! {
self.authorArray.append(info.author)
self.descriptionArray.append(info.description)
if info.publishedAt != nil {
self.publishedAtArray.append(info.publishedAt)
}
self.titleArray.append(info.title)
self.urlArray.append(info.url)
self.urlToImageArray.append(info.urlToImage)
print(self.authorArray)
}
}
you have to make separate function for this calculation and try to avoid the any calculate functionality in "cellForRowAt"
I have an array of arrays, theMealIngredients = [[]]
I'm creating a newMeal from a current meal and I'm basically copying all checkmarked ingredients into it from the right section and row from a tableview. However, when I use the append, it obviously doesn't know which section to go in as the array is multidimensional. It keeps telling me to cast it as an NSArray but that isn't what I want to do, I don't think.
The current line I'm using is:
newMeal.theMealIngredients.append((selectedMeal!.theMealIngredients[indexPath.section][indexPath.row])
You should re-model your data to match its meaning, then extract your tableview from that. That way you can work much more easily on the data without having to fuss with the special needs of displaying the data. From your description, you have a type, Meal that has [Ingredient]:
struct Meal {
let name: String
let ingredients: [Ingredient]
}
(None of this has been tested; but it should be pretty close to correct. This is all in Swift 3; Swift 2.2 is quite similar.)
Ingredient has a name and a kind (meat, carbs, etc):
struct Ingredient {
enum Kind: String {
case meat
case carbs
var name: String { return self.rawValue }
}
let kind: Kind
let name: String
}
Now we can think about things in terms of Meals and Ingredients rather than sections and rows. But of course we need sections and rows for table views. No problem. Add them.
extension Meal {
// For your headers (these are sorted by name so they have a consistent order)
var ingredientSectionKinds: [Ingredient.Kind] {
return ingredients.map { $0.kind }.sorted(by: {$0.name < $1.name})
}
// For your rows
var ingredientSections: [[Ingredient]] {
return ingredientSectionKinds.map { sectionKind in
ingredients.filter { $0.kind == sectionKind }
}
}
}
Now we can easily grab an ingredient for any given index path, and we can implement your copying requirement based on index paths:
extension Meal {
init(copyingIngredientsFrom meal: Meal, atIndexPaths indexPaths: [IndexPath]) {
let sections = meal.ingredientSections
self.init(name: meal.name, ingredients: indexPaths.map { sections[$0.section][$0.row] })
}
}
Now we can do everything in one line of calling code in the table view controller:
let newMeal = Meal(copyingIngredientsFrom: selectedMeal,
atIndexPaths: indexPathsForSelectedRows)
We don't have to worry about which section to put each ingredient into for the copy. We just throw all the ingredients into the Meal and let them be sorted out later.
Some of this is code is very inefficient (it recomputes some things many times). That would be a problem if ingredient lists could be long (but they probably aren't), and can be optimized if needed by caching the results or redesigning the internal implementation details of Meal. But starting with a clear data model keeps the code simple and straightforward rather than getting lost in nested arrays in the calling code.
Multi-dimentional arrays are very challenging to use well in Swift because they're not really multi-dimentional. They're just arrays of arrays. That means every row can have a different number of columns, which is a common source of crashing bugs when people run off the ends of a given row.
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)