How to store nested dictionary value - ios

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.

Related

Convert the original tableview to collectionview data cannot be passed

I want to replace the original tableview with collectionview, the code of the original tableview:
let selectedRow = MarketView.indexPathForSelectedRow!.row
I'm learning online to change to this code and I get an error:
let selectedRow = MarketView.indexPathsForSelectedItems!.first
The error shows:
Cannot convert value of type 'IndexPath?' to expected argument type 'Int'
This is the complete code as shown in the figure
I just learned to use collectionview, how should I modify it, thank you for your help
Unlike indexPathForSelectedRow which returns a single index path indexPathsForSelectedItems returns an array of index paths
And row is not first, the collection view equivalent of row – as the name of the API implies – is item, you have to write indexPathsForSelectedItems!.first!.item.
But it's not recommended to force unwrap the objects. A safer way is
guard let selectedRow = MarketView.indexPathsForSelectedItems?.first?.item else { return }

Int vs. IndexPath Troubles

I am writing a function to loop through all of the cells in a UITableViewController. Here is what I have so far:
var i = 0
while (i < tableView.numberOfRows(inSection: 0)) {
i += 1
let cell = tableView.cellForRow(at: i-1)
}
Everything loops correctly until I try to get the cell. It expects an input of type IndexPath, however I am passing in an Int. Whenever I force it as an IndexPath like so:
let cell = tableView.cellForRow(at: i-1 as! IndexPath)
I get a warning saying that it will always fail/ always return nil. Is there a better way to do this, or am I just missing a crucial step. Any help is greatly appreciated. Thanks in advance!
EDIT (A LITTLE MORE EXPLANATION): All the cells are custom classed cells, with a specific variable. I want to loop through all of the cells and get that value.
let visibleCells = tableView.visibleCells
for aCell in visibleCells {
print(aCell.question?.text) <------- this is the value I want
}
Most of the information below has been said by others in the other answers and comments, but I wanted to put it all in one place:
We need to step back from the specifics of the question and ask what you are actually trying to do.
The UITableView method cellForRow(at:) will only return cells that are actually on-screen. If there is only room for 5 cells and you have to scroll to expose the rest, that method will return nil for all but the cells that are visible.
As others have suggested, if your goal is to loop through the cells that are on-screen the property tableView.visibleCells would be a better choice.
If your goal is to loop through all cells that exist in your data then you need to explain what you are trying to do.
As for your specific question, the cellForRow(at:) wants a parameter of type IndexPath. You can't simply cast an Int to IndexPath. That will fail. Instead, as #TaylorM said in their answer, you need to create an IndexPath. If your table view only has a single section then you can simply use
let indexPath = IndexPath(row: i, section: 0)
(Assuming you fix your loop code so your indexes start at 0.)
It also does not make sense to use a while loop like that.
Instead of all of this, I suggest using:
let visibleCells = tableView.visibleCells
for aCell in visibleCells {
//Do something with the cell
}
You should create an IndexPath via code, like this:
let ndx = IndexPath(row:i, section: 0)
Or, to modify your code:
var i = 0
while (i < tableView.numberOfRows(inSection: 0)) {
i += 1
let ndx = IndexPath(row:i-1, section: 0)
let cell = tableView.cellForRow(at:ndx)
}
Based on the later edit where you mention that you want the value of a text string on each row, I would suggest that the above is probably not the best way to approach this :) (I know others have already said this, but I didn't want to make assumptions about what you wanted to do unless you specifically stated what you wanted ...)
You are probably better off taking the same approach you take to populate the data for the table view cells via cellForRowAt: to get the question text than to loop through all the table rows, which would result in some issues for non-visible rows as others have indicated already.
If you have any issues with getting the data provided for cellForRowAt:, do share the code for the cellForRowAt: with us and I'm sure one of us can help you figure things out :)
Try
let indexPath = IndexPath(row: i, section: 0)
You may need to adjust the section to fit your needs.
Check out the documentation for IndexPath. Like most classes and structures, IndexPath has initializers. If you need to create a new instance of any object, you'll most like use an initializer.
*also see the comment from #rmaddy:
"Unrelated to your question but I can almost guarantee that your attempt to loop through all of the cells is the wrong thing to do. What is your actual goal with that loop?"
I believe I have figured it out using all of the help from you wonderful people.
In my case, I have a custom class with a variable that I want access to. I can do what Taylor M, Fahim, and Duncan C said: let indexPath = IndexPath(row: i, section: 0)
Then I can add as! NumberTableViewCell to the end, which allows me access to the values defined there. In the end, it looked like this:
var i = 0
while (i < tableView.numberOfRows(inSection: 0)) {
i += 1
var cell = tableView.cellForRow(at: IndexPath(row: i-1, section: 0)) as! NumberTableViewCell
print(cell.question.text!)
}
Thank you all for your help!

Closure does not run in the main thread in swift

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"

App crashes if playlist count = 0 (Do empty playlists have a persistentid?)

My app is crashing if a playlist is empty (no songs). My app works for all non-empty playlists. It seems like there isn't a persistentid for an empty playlist, but I think I am wrong on that.
let qryPlaylists = MPMediaQuery.playlistsQuery()
var selectedPlaylistTitle: String!
var selectedPlaylistPersistentID: UInt64!
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let indexPath: NSIndexPath = playlistsTableView.indexPathForSelectedRow!
let rowItem = qryPlaylists.collections![indexPath.row]
let playlistSize = rowItem.count
print("playlistSize = ", playlistSize)
selectedPlaylistTitle = rowItem.valueForProperty(MPMediaPlaylistPropertyName) as! String
print("selectedPlaylistTitle = ", selectedPlaylistTitle)
// Will crash on this next line if the playlistSize = 0
selectedPlaylistPersistentID = rowItem.items[0].persistentID
// If I use following line instead, it will crash on any playlist
// selectedPlaylistPersistentID = rowItem.valueForProperty(MPMediaPlaylistPropertyPersistentID) as! UInt64
// This line will never be printed
print("selectedPlaylistPersistentID = ", selectedPlaylistPersistentID)
}
Thanks in advance for any help!
If an array such as items is empty, it has no index 0 and will crash if you refer to it, with an out-of-bounds error. So if you don't want to crash, don't do that. You already know that items is empty, because rowItem.count told you so; as you said yourself, playlistSize is 0 in that case.
A simple-minded way to look at it is this: the largest legal index in an array is one less than its count.
Another issue you asked about is that this line always crashes:
selectedPlaylistPersistentID = rowItem.valueForProperty(MPMediaPlaylistPropertyPersistentID) as! UInt64
The problem here is that you are apparently using Swift 2.x. (You should have said that in your question; I deduce it, though, from the fact that valueForProperty has not changed to value(forProperty:), which is what it is called in Swift 3.)
In Swift 2, you cannot cast directly down to UInt64. (I am surprised that the compiler does not draw your attention to this fact with a warning.) Thus, the cast fails and you crash. You need to cast down to NSNumber and then take that NSNumber's unsignedLongLongValue.
And while you are doing this, you really should stop using exclamation marks in your code. When I say "cast down", I mean "cast down safely". My own Swift 2 code for doing this sort of thing looks like this:
if let id = (rowItem.valueForProperty(MPMediaItemPropertyAlbumPersistentID) as? NSNumber)?.unsignedLongLongValue {
// ...
}

How to Update uitableview from CloudKit using discoverAllIdentities

So, I am new to cloudKit and to working with multiple threads in general, which I think is the source of the problem here, so if I simply need to research more, please just comment so and I will take that to heart.
Here is my question:
I am working in Swift 3 Xcode 8.1
I have in my view controller this variable:
var contactsNearby: [String:CLLocation]?
Then at the end of ViewDidLoad I call one of my view controllers methods let's call it:
populateContactsNearby()
inside that method I call:
container.discoverAllIdentities(completionHandler: { (identities, error) in
for userIdentity in identities! {
self.container.publicCloudDatabase.fetch(withRecordID: userIdentity.userRecordID!, completionHandler: { (userRecord, error) in
let contactsLocation = userRecord?.object(forKey: "currentLocation")
if self.closeEnough(self.myLocation!, contactLocation: contactsLocation as! CLLocation) {
var contactsName = ""
contactsFirstName = userIdentity.nameComponents?.givenName
if contactsName != "" && contactsLocation != nil {
self.contactsNearby?["\(contactsName)"] = contactsLocation as? CLLocation
}
}
})
}
})
}
I apologize if I am missing or have an extra bracket somewhere. I have omitted some error checking code and so forth in order to get this down to bare-bones. So the goal of all that is to populate my contactsNearby Dictionary with data from CloudKit. A name as the key a location as the value. I want to use that data to populate a tableview. In the above code, the call to closeEnough is a call to another one of my view controllers methods to check if the contact from CloudKit has a location close enough to my user to be relevant to the apps purposes. Also myLocation is a variable that is populated before the segue. It holds the CLLocation of the app users current location.
The Problem:
The if statement:
if contactsName != "" && contactsLocation != nil { }
Appears to succeed. But my view controllers variable:
var contactsNearby: [String:CLLocation]?
Is never populated and I know there is data available in cloudKit.
If it's relevant here is some test code that I have in cellForRowAtIndexPath right now:
let contact = self.contactsNearby?.popFirst()
let name = contact?.key
if name != nil {
cell.textLabel?.text = name
}else {
cell.textLabel?.text = "nothing was there"
}
My rows alway populate with "nothing was there". I have seen answers where people have done CKQueries to update the UI, but in those answers, the user built the query themselves. That seems different from using a CloudKit function like discoverAllIdentities.
I have tried to be as specific as possible in asking this question. If this question could be improved please let me know. I think it's a question that could benefit the community.
Okay, I need to do some more testing, but I think I got it working. Thank you Paulw11 for your comment. It got me on the right track.
As it turns out there were 2 problems.
First, as pointed out I have an asynchronous call inside a for loop. As recommended, I used a dispatchGroup.
Inside the cloudKit call to discoverAllIdentities I declared a dispatchGroup, kind of like so:
var icloudDispatchGroup = DispatchGroup()
Then just inside the for loop that is going to make an async call, I enter the dispatchGroup:
icloudDispatchGroup.enter()
Then just before the end of the publicCloudDatabase.fetch completion handler I call:
icloudDispatchGroup.leave()
and
icloudDispatchGroup.wait()
Which, I believe, I'm still new to this remember, ends the dispatchGroup and causes the current thread to wait until that dispatchGroup finishes before allowing the current thread to continue.
The Above took care of the multithreading issue, but my contactsNearby[String:CLLocation]? Dictionary was still not being populated.
Which leads me to the 2nd problem
At the top of my view controller I declared my Dictionary:
var contactsNearby: [String: CLLocation]?
This declared a dictionary, but does not initialize it, which I don't think I fully realized, so when I attempted to populate it:
self.contactsNearby?["\(contactsName)"] = contactsLocation as? CLLocation
It quietly failed because it is optional and returned nil
So, in viewDidLoad before I even call populateContactsNearby I initialize the dictionary:
contactsNearby = [String:CLLocation]()
This does not make it cease to be an optional, which Swift being strongly typed would not allow, but merely initializes contactsNearby as an optional empty Dictionary.
At least, that is my understanding of what is going on. If anyone has a more elegant solution, I am always trying to improve.
In case you are wondering how I then update the UI, I do so with a property observer on the contactsNearby Dictionary. So the declaration of the dictionary at the top of the view controller looks like this:
var contactsNearby: [String: CLLocation]? {
didSet {
if (contactsNearby?.isEmpty)! || contactsNearby == nil {
return
}else{
DispatchQueue.main.sync {
self.nearbyTableView.reloadData()
}
}
}
}
I suppose I didn't really need to check for empty and nil. So then in cellForRowAtIndexPath I have something kind of like so:
let cell = tableview.dequeueReusableCell(withIdentifier: "nearbyCell", for: indexPath)
if contactsNearby?.isEmpty == false {
let contact = contactsNearby?.popFirst()
cell.textLabel?.text = contact?.key
}else {
cell.textLabel?.text = "Some Placeholder Text Here"
}
return cell
If anyone sees an error in my thinking or sees any of this heading for disaster, feel free to let me know. I still have a lot of testing to do, but I wanted to get back here and let you know what I have found.

Resources