Appending NSDictionary to [NSDictionary] gives unexpected results - ios

I've got some [NSDictionary] filled with books that I'm checking values for to better display the content in a UICollectionView.
I'm doing a check on a key if it contains several ISBN numbers. If it do I'll want to display them as separate books. The key is 'isbn' and if the value are something like '["9788252586336", " 9788203360510"]' I'll want to display them as separate books. So they'll become two "equal" books, but with different isbn numbers.
Here's what happens:
Heres the code
func parserFinishedSuccesfully(data: NSMutableArray){
self.tempArray = []
if !data.isEqual(nil) && data.count > 0
{
for i in (0 ..< data.count)
{
var currentBook = NSDictionary()
if !((data[i] as? NSDictionary)!.valueForKey("isbn") as? String)!.isEmpty
{
//If the book do have isbn then I'll want to display it
currentBook = data[i] as! NSDictionary
if let isbn = currentBook.valueForKey("isbn") as? String
{ //isbn could be "9788252586336; 9788203360510"
let isbnNumbersRecieved = isbnNumbers(isbn)
//gives: ["9788252586336", " 9788203360510"]
if isbnNumbersRecieved.count >= 2
{
//We are dealing with several isbn numbers on one book
for isbn in isbnNumbersRecieved
{ //The problem lies here!
print("This is the isbn: \(isbn)")
currentBook.setValue(isbn, forKey: "isbn")
self.tempArray.append(currentBook)
}
}
else
{
//Only one isbn
//currentBook.setValue(isbnNumbersRecieved.first, forKey: "isbn")
//self.tempArray.append(currentBook)
}
}else{
//No isbn, no want
print("No isbn")
}
}
}
print(self.tempArray)
self.collectionView.reloadData()
}
In code it says: "problem lies here". Well...the problem is there.
When it enters that loop with this array ["9788252586336", " 9788203360510"] it will first print the first number, then the second as it should.
Then when it takes currentBook. I change the value that is there before which would be "9788252586336; 9788203360510" with just the first isbn number "9788252586336". And then I take the entire currentBook and append it to the array (tempArray) that will be used to display the books in a UICollectionView later.
When the second iteration starts the isbn number will be "9788203360510" and I'll set the isbn number to be that in currentBook, just like the first iteration.
The weird thing happens now. When currentBook get's appended to (tempArray) it should contain the same book, but with different isbn numbers. But it actually contains the same fu#%$ book with same isbn number.
Somehow the second iteration adds 2 books with the same isbn number and the first book that should be there is gone...
Picture 1 is after the first iteration and picture 2 is after the second iteration. If you look at the key "isbn", you can see that there is something messed up with the code...
Picture 1
Picture 2
How can this happen and how would I fix it?

This is happening because currentBook is a reference type, not a value type. You are adding two references to the same book, currentBook, to the array.
As a simple fix, you could copy() the dictionary before modifying it and adding it to the array. A more robust fix would be to make all of your models value types instead.
Further reading:
Swift Blog: Value and Reference Types
Ray Wenderlich: Reference vs. Value Types in Swift

You are using the same dictionary object for each item in your for loop and hence this result. Try below and let me know if it works.
//We are dealing with several isbn numbers on one book
for isbn in isbnNumbersRecieved
{ //The problem lies here!
print("This is the isbn: \(isbn)")
let tempDict = NSDictionary()
tempDict.setValue(isbn, forKey: "isbn")
//currentBook.setValue(isbn, forKey: "isbn")
self.tempArray.append(tempDict)
}

Related

Realm iOS: count the distinct objects in a collection

What is the most efficient way to get the count of the unique values on a table?
For example:
fruitType
---------
banana
banana
apple
apple
apple
bananas : 2
apples : 3
By using fruitsCollection.distinct(by: ["fruitType"]) i can get the distinct values but not the count.
Any help would be appreciated.
you could try something simple like this (assuming fruits are Strings, adjust accordingly to your object types):
let fruits = fruitsCollection.distinct(by: ["fruitType"])
var results = [String:Int]()
Set(fruits).forEach{ fruit in results[fruit] = (fruits.filter{$0 == fruit}).count }
print("---> results: \(results)")
or
let results: [String:Int] = Set(fruits).reduce(into: [:]) { dict, next in
dict[next] = (fruits.filter{$0 == next}).count }
print("---> results: \(results)")
The Set(fruits) gives you the unique set of fruit names. The filter{...} gives you the count of each. The forEach or the reduce turns the results into a dictionary of key values.
#workingdog answer works very well but here's a more Realmy option. Something to keep in mind is that Realm Results objects are lazily loaded - meaning working with very large datasets has a low memory impact.
However, as soon high level Swift functions are used, that lazy-ness is lost and every object gobbles up memory.
For example, loading 50,000 Realm objects into a Results object doesn't have any significant memory impact - however, loading 50,000 objects into an Array could overwhelm the device as the objects loose their lazy-loading nature.
With this solution, we rely on Realm to present unique values and store them in Results (lazy!) then iterating over those we filter for matching objects (lazy) and return their count.
I created a FruitClass to hold the fruit types
class FruitClass: Object {
#Persisted var fruitType = ""
}
and then to code
This is a very memory friendly solution
//get the unique types of fruit. results are lazy!
let results = realm.objects(FruitClass.self).distinct(by: ["fruitType"])
//iterate over the results to get each fruit type, and then filter for those to get the count of each
for fruit in results {
let type = fruit.fruitType
let count = realm.objects(FruitClass.self).filter("fruitType == %#", type).count
print("\(type) has a count of \(count)")
}
and the results
apple has a count of 3
banana has a count of 2
orange has a count of 1
pear has a count of 1

How do I append to a section of an array? [[]]

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.

Filter an array by NOT containing string (swift 2)

I am relatively new to swift; I am working on filtering arrays.
I know how to filter out elements of an array that contain a letter (like so: let filteredList = wordlist.filter { !$0.characters.contains(letter) }), but how do I filter out elements that do not have a letter?
Here's what I want to accomplish:
I have a word list in string-array format, i.e. ["thing", "other thing"] (but much longer), and I want to return every element that has a certain letter, filtering out the ones that do not have a certain letter.
Thanks in advance.
This was a silly question, I am sorry. Anyway, I just needed to remove the exclamation mark. So...
let filteredList = wordlist.filter { !$0.characters.contains(letter) }
// returns elements in the array WITHOUT "letter".
let filteredList = wordlist.filter { $0.characters.contains(letter) }
// returns elements in the array WITH "letter".
Thanks Eendje.

Multidimensional Array Looping in cellForRowAtIndexPath Swift

I have a multidimensional array that I want to display the values of onto one UILabel in each respective cell.
My arrays look like this:
var arrayExample = [["beverages", "food", "suppliers"]["other stuff, "medicine"]]
I'm looping through these values in the cellForRowAtIndexPath in order for it to display on different cells (on a UILabel) the appropriate values:
if let onTheLabel: AnyObject = arrayOfContactsFound as? AnyObject {
for var i = 0; i < objects!.count; i++ {
cell?.contactsUserHas.text = "\(onTheLabel[indexPath.row][i])" as! String
print("arrayOfContactsFound Printing! \(onTheLabel)")
}
}
When printing to the console I get:
arrayOfContactsFound Printing! (
(
beverages,
"supply chain",
pharmacuticals
)
)
But on my label I get "beverages". That's it. How can I get the other 2 values (or X amount if there are more or less than 3 values)?
My for in loop is obviously not doing the trick. Assuming I can optimize/fix that to display all the values?
Thanks in advance.
In your loop you're setting the text of your label multiple times. Each time you set it it doesn't accumulate, it completely replaces the current text with the new text. You'll want something like this:
// Remove the cast and the loop in your code example, and replace with this
let items = arrayOfContactsFound[indexPath.row]
let itemsString = items.joinWithSeparator(" ")
cell?.contactsUserHas.text = itemsString
Another thing to note is your cast doesn't quite make a lot of sense.
var arrayExample = [["beverages", "food", "suppliers"]["other stuff, "medicine"]]
So arrayExample is of type [[String]]. I'm assuming each cell in your table view represents one of the arrays of strings in your array. So each cell represents one [String]. So your items should be arrayExample[indexPath.row]. The cast to AnyObject doesn't make too much sense. If anything you'd be casting it to [[AnyObject]], but there's no reason to because the compiler should already know it's [[String]].

Confused on snippet of code for implementing iCloud behavior on iOS

The code is from a book. In terms of overall app architecture (MVC), it's part of the Model. The model has two main components:
An array of tags called tags
A dictionary of tag - query called searches
The app saves these pieces of data in the NSUserDefaults (iOS defaults system) and on iCloud. The following method is called when a change in iCloud is signaled. The parameter is an instance of NSNotification.userInfo
// add, update, or delete searches based on iCloud changes
func performUpdates(userInfo: [NSObject: AnyObject?]) {
// get changed keys NSArray; convert to [String]
let changedKeysObject = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey]
let changedKeys = changedKeysObject as! [String]
// get NSUbiquitousKeyValueStore for updating
let keyValueStore = NSUbiquitousKeyValueStore.defaultStore()
// update searches based on iCloud changes
for key in changedKeys {
if let query = keyValueStore.stringForKey(key) {
saveQuery(query, forTag: key, saveToCloud: false)
} else {
searches.removeValueForKey(key)
tags = tags.filter{$0 != key}
updateUserDefaults(updateTags: true, updateSearches: true)
}
delegate.modelDataChanged() // update the view
}
}
My question is on the if - else inside the for loop. The for loop iterates over keys that where changed; either the user adds a new search, updates an existing search, or deletes a search. But, I don't understand the logic behind the if-else. Some clarifying thoughts would be appreciated. I've read it over and over but it doesn't tick with me.
if let query = keyValueStore.stringForKey(key)
means that if keyValueStore contains a string corresponding to key, then this string will be assigned to the constant query.
This is called "safe unwrapping":
inside the if let ... condition, the query is safely saved with saveQuery because using if let ... guarantees that the value of keyValueStore.stringForKey(key) won't be nil.
If the value is nil, then in the else branch, the filter method is used to update the tags array without the key we just processed: tags.filter{$0 != key} means "return all items in tags that are different from key" (the $0 represents the current item from the array processed by filter).

Resources