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"
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.
Given an optional array of type Book, declared in a table view controller class:
var books: [Book]?
Later in cellForRowAtIndexPath I have:
let cell = tableView.dequeueReusableCell(withIdentifier: "cellID", for: indexPath)
let book = books?[indexPath.row]
cell.textLabel?.text = book?.title
return cell
}
Now, so far I have only seen this books array optional unwrapped using if let statement in order to loop through the array.
But why in cellForRow here do we not unwrap books and unwrap book instead of adding another ? after it, as in books?[indexPath.row] and cell.textLabel?.text = book?.title
You don't have to unwrap books or book because the text property of the UILabel is an optional String?. The only time you have to unwrap optionals is if you're using the retrieved value in some context that doesn't accept an optional. But text does, so your example operates fine.
My problem with this syntax is that, while it's a useful shorthand, this is a bit sloppy. If there is a programming problem (for example, books is nil when this is called even though that technically shouldn't be possible), the code in the question will silently continue executing and you will be left scratching your head, wondering why the label in the cell was blank. (This is also true for patterns that suggest using guard statements, quietly returning the cell if the unwrapping failed.)
But I strongly disagree with suggestions provided elsewhere that books should not be an optional. If this array cannot be populated at the time the view controller is instantiated, then books should be an optional (and be nil) until such time that this information is retrieved.
Bottom line, there is a difference between state of "books has not yet been set" and "books was populated, but no records were returned". This is the purpose of optionals, to avoid arbitrary sentinel values like "empty array" to indicate that books does not have its value set. (I am sympathetic to those who object to the sloppy overuse of optionals, but this simply is not one of those cases. And I'm not buying the Optional performance overhead concern, either, because it is immaterial in this example and it strikes me as a perfect example of premature optimization.)
I believe, instead, that you should (a) leave books optional; but (b) detect if it is unexpectedly nil and report this as the error it is, because if cellForRowAt is called, it must not be nil.
Thus, I would suggest:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BookCell", for: indexPath)
let book = books![indexPath.row]
cell.textLabel?.text = book.title
return cell
}
This accomplishes what we want, populating the label correctly, but will also immediately take the developer to the offending line of code if there was a code logic error and books was nil.
Now, many developers have a maniacal aversion to using the ! forced unwrapping operator. Personally I think it's fine because it's a fundamental programming error if books was nil when this method was called, and the forced unwrapping operator will stop my execution at the offending line and I'll know precisely what the issue is.
But, if you really want to avoid the forced unwrapping operator, !, then use a guard statement, triggering an informative fatalError if books is nil:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BookCell", for: indexPath)
guard let book = books?[indexPath.row] else {
fatalError("No book found")
}
cell.textLabel?.text = book?.title
return cell
}
The key is that if a situation arose where books was nil, you want to know immediately in the development process, not left guessing what the source of the problem was.
To be clear, this forced unwrapping and/or fatalError pattern should never be used in situations where the unwrapping could ever fail for reasons outside of your control. For example, don't use these patterns when parsing responses from remote web service or processing user input. You want to gracefully handle errors that might have resulted outside of your control. But if it is a true program error for a particular scenario to take place, then forced unwrapping and/or fatalError approaches are advisable so logic errors are immediately apparent, rather than forcing you to hunt around looking for the source of aberrant app behavior.
An optional unwrap using ? means that if the value exists, unwrap it. This is ok to do (safe to do) if you don't plan on using the potentially nil value elsewhere. In your case, you could technically do it this way (it will compile, and likely won't crash), but it's not the safest way, because now you're relying on each piece along the way to support a nil value.
That said, you don't really need to declare your array as optional. Because you're enumerating the number of rows of data anyway, the array should always exist. The variable is the number of rows it contains, not whether it exists or not. In other words: checking that the array exists on top of checking the number of rows is just extra work for the compiler and extra code for you to write.
You can also take advantage of guard too, reducing the amount of code that executes if you don't have good data:
// ...
guard let book = books[indexPath.row] else { return cell }
cell.textLabel?.text = book.title
return cell
Never declare a data source array of a non-optional table view or collection view as optional.
var books = [Book]()
That makes your life so much easier
let cell = tableView.dequeueReusableCell(withIdentifier: "cellID", for: indexPath)
let book = books[indexPath.row]
cell.textLabel?.text = book.title
return cell
}
I want to store the value of a nested dictionary so my collection view can use it later without having to loop through stuff to find it every time. I also don't think it'll work (i could be wrong tho) if i try to put the loop in my cellforindexatpath function. The "let abilitiesArray: [Ability] =....." is inside my class
Ultimately I want abilitiesArray = abilities. "heroForDetails.hero.abilities" is a [String: [Ability]] so I want to return the [Ability] whose heroForDetails.hero.id == heroId because there are multiple key: value pairs in heroForDetails.hero.abilities. The current error is missing return in closure expected to return [Ability].
let abilitiesArray: [Ability] = { () -> ([Ability]) in
let val = heroForDetails.hero.abilities
for (heroId, abilities) in val {
if heroForDetails.hero.id == heroId {
return abilities
}
}
}()
"abilities" is an array that I plan to use like so
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCellWithReuseIdentifier("AbilitiesCell", forIndexPath: indexPath) as? AbilitiesCell {
let ability: Ability
ability = abilitiesArray[indexPath.row]
cell.configureCell(ability)
return cell
}else {
return UICollectionViewCell()
}
}
I hope i did a good job at explaining this. I just started learning to code so Any help would be great. I could be going about this completely wrong too.
In your first code snippet, you seem to be finding an item in the val dictionary using a key. I hope that's what you want to do. To do that, you don't need to loop through the whole dictionary. It's very slow to do so. (O(n)) You can use the subscript to access the dictionary using a key (the heroForDeails.hero.id variable). This is much faster (O(1)).
You can shorten the whole snippet into one line:
let abilitiesArray = heroForDetails.hero.abilities[heroForDetails.hero.id]!
Also, I don't see a question in your post...
You can access the dictionary by
let val = heroForDetails.hero.abilities
let abilities = val[heroForDeails.hero.id]
As far as accessing it in your cellforindexatpath, it should be just fine. Don't worry about the closure.
I am trying to filter the posts based on their profile. For instance, when I go to my profile I only want to see my posts, not all the posts in my database. I attempted to make a filter for that but the code below does not seem to work and I am unsure as to why that is. It may be something obvious but I can not seem to pinpoint the issue, any ideas?
I have attached a picture of the database to further assist anybody.
The code runs perfectly fine it just does not filter the usernames.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var user = PFUser.currentUser()?.username!
let bucketCellObj = tableView.dequeueReusableCellWithIdentifier("bucket", forIndexPath: indexPath) as! BucketTableViewCell
var query = PFQuery(className: "Bucket")
query.whereKey("creator", equalTo: user!)
query.findObjectsInBackgroundWithBlock { (PFObject, error) -> Void in
if error == nil {
bucketCellObj.bucketname.numberOfLines = 0
bucketCellObj.username.text = self.bucketsArray[indexPath.row]["creator"] as? String
bucketCellObj.bucketname.text = self.bucketsArray[indexPath.row]["name"] as? String
bucketCellObj.selectionStyle = .None
} else {
print("ERROR")
}
}
return bucketCellObj
}
What you are doing here might work under some circumstances but will certainly bite you at some point.
What your code does:
show some arbitrary number of cells - probably based on self.bucketsArray.count or something similar
in each cell creation, run a parse query
when the query returns, customize the already displayed cell accordingly - without any usage of the requested query response
That will cause problems for a couple of reasons:
you perform too many requests, each cell requests some data, each new displayed cell requests its own data again
you display the old data in the cell until the new data is fetched which could take a few seconds due the amount of requests
you could encouter a problem where you requests some data for a cell, that cell moves off-screen, gets reused, then the first query returns, still holds the reference to it and will therefore display wrong data
How it can be solved
Do not requests the data in the cellForRowAtIndexPath.
Request the data once in viewDidLoad or similar. as soon as the data gets returned, parse it and initiate a tableView.reload().
In the cellForRowAtIndexPath make use of the already retrieved data, do not perform anymore async tasks.
My question is very similar to several others here but I just can't get it to work. I'm making an API call via a helper class that I wrote.
First I tried a standard function with a return value and the result was as expected. The background task completed after I tired to assign the result.
Now I'm using a closure and I can get the value back into my view controller but its still stuck in the closure, I have the same problem. I know I need to use GCD to get the assignment to happen in the main queue.
this is what I have in my view controller
var artists = [String]()
let api = APIController()
api.getArtistList("foo fighters") { (thelist) -> Void in
if let names = thelist {
dispatch_async(dispatch_get_main_queue()) {
artists = names
print("in the closure: \(artists)")
}
}
}
print ("method 1 results: \(artists)")
as the results are:
method 1 results: []
in the closure: [Foo Fighters & Brian May, UK Foo Fighters, John Fogerty with Foo Fighters, Foo Fighters, Foo Fighters feat. Norah Jones, Foo Fighters feat. Brian May, Foo Fighters vs. Beastie Boys]
I know why this is happening, I just don't know how to fix it :( The API calls need to be async, so what is the best practice for capturing these results? Based on what the user selects in the table view I'll be making subsequent api calls so its not like I can handle everything inside the closure
I completely agree with the #Craig proposal of the use of the GCD, but as your question involves the request of the API call every time you select a row, you can do the following:
Let's suppose you use the tableView:didSelectRowAtIndexPath: method to handle the selection, then you can do the following inside it:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// it is just a form to get the item
let selectedItem = items.objectAtIndex(indexPath.row) as String
api.getArtistList(selectedItem) { (thelist) -> Void in
if let names = thelist {
dispatch_async(dispatch_get_main_queue()) {
artists = names
}
}
}
}
And then you can observe the property and handle do you want inside it :
var artists: [String] = [] {
didSet {
self.tableView.reloadData() // or anything you need to handle.
}
}
It just another way to see it. I hope this help you.
The easy solution is to do whatever you're doing at your print(), inside the closure.
Since you're already dispatch_asyncing to the main queue (the main/GUI thread), you can complete any processing there. Push a new view controller, present some modal data, update your current view controller, etc.
Just make sure that you don't have multiple threads modifying/accessing your local/cached data that is being displayed. Especially if it's being used by UITableViewDelegate / UITableViewDataSource implementations, which will throw fits if you start getting wishy-washy or inconsistent with your return values.
As long as you can retrieve the data in the background, and the only processing that needs to occur on the main thread is an instance variable reassignment, or some kind of array appending, just do that on the main thread, using the data you retrieved on the back end. It's not heavy. If it is heavy, then you're going to need more sophisticated synchronization methods to protect your data.
Normally the pattern looks like:
dispatch_async(getBackgroundQueue(), {
var theData = getTheDataFromNetwork();
dispatch_async(dispatch_get_main_queue() {
self.data = theData // Update the instance variable of your ViewController
self.tableView.reloadData() // Or some other 'reload' method
});
})
So where you'd normally refresh a table view or notify your ViewController that the operation has completed (or that local data has been updated), you should continue your main-thread processing.