Storing and working with huge dictionaries in Swift - ios

I am trying to load a dictionary with around 11,000 pairs into a Swift program (about .7MB). The values of the dictionary are arrays, and I need to be able to loop through the dictionary's arrays and compare those values with values in another array. There is an average of 10 items in the subarrays, although some have two or three, and some have hundreds.
The program crashes, and I am looking for a way to fix it while keeping the functionality from my Python-based prototype. I was thinking of using a bunch of property lists, and loading them into memory one by one, but this seems inefficient. What can I do to redesign this?
Some of the code which is being problematic:var dictOfHashesAndMaterials:[String:[Int]]: = ["10928123sdfb234w3fw3": [123,435,1,573,456,3234,653,57856,434,345],"13435fw323df0239dfsdf":[435,756,978,231,5668,485,9678,1233,87,989]] // and 11,000 additional entries which look similar to this.
And here's where I use it:` // function to populate the list of ingredients.
func populateListOfRecipes(){
var storedIngredients = UserDefaults.standard.object(forKey: "ingredients") as! [Int]
for(key, value) in dictOfHashesAndMaterials{
let listSet = Set(storedMaterials)
let findListSet = Set(value)
if findListSet.isSubset(of: listSet){
print("found a match!")
arrayOfAvailableProjects.append(key)
}
}
tableView.reloadData()
}`
A tableView is then populated by cells which link to the specifics of a given project. The details are filled in when the hashes are sent to the server, and the corresponding instructions are returned in full to the app.

Is it crashing in the simulator? I've worked with 30 GB files using the simulator without a crash. On the device is a different story...
Anyway, try this:
for(key, value) in dictOfHashesAndMaterials{
autoreleasepool {
let listSet = Set(storedMaterials)
let findListSet = Set(value)
if findListSet.isSubset(of: listSet){
print("found a match!")
arrayOfAvailableProjects.append(key)
}
}
}
tableView.reloadData()
}
By draining the auto release pool every iteration you will prevent running out of available memory.

Related

"Preloading" A Dictionary With Keys in Swift

This is a fairly simple issue, but one I would like to solve, as it MAY help with performance.
I want to find out if Swift has a way to create a Dictionary, specifying ONLY keys, and maybe no values, or a single value that is set in each entry.
In other words, I want to create a Dictionary object, and "preload" its keys. Since this is Swift, the values could be 0 or nil (or whatever is a default empty).
The reason for this, is so that I can avoid two loops, where I go through once, filling a Dictionary with keys and empty values, and a second one, where I then set those values (There's a practical reason for wanting this, which is a bit out of the scope of this question).
Here's sort of what I'm thinking:
func gimme_a_new_dictionary(_ inKeyArray:[Int]) -> [Int:Int] {
var ret:[Int:Int] = [:]
for key in inKeyArray {
ret[key] = 0
}
return ret
}
let test1 = gimme_a_new_dictionary([4,6,1,3,0,1000])
But I'm wondering if there's a quicker way to do the same thing (as in "language construct" way -I could probably figure out a faster way to do this in a function).
UPDATE: The first solution ALMOST works. It works fine in Mac/iOS. However, the Linux version of Swift 3 doesn't seem to have the uniqueKeysWithValues initializer, which is annoying.
func gimme_a_new_dictionary(_ inKeyArray:[Int]) -> [Int:Int] {
return Dictionary<Int,Int>(uniqueKeysWithValues: inKeyArray.map {($0, 0)})
}
let test1 = gimme_a_new_dictionary([4,6,1,3,0,1000])
For Swift 4, you can use the dictionary constructor that takes a sequence and use map to create the sequence of tuples from your array of keys:
let dict = Dictionary(uniqueKeysWithValues: [4,6,1,3,0,1000].map {($0, 0)})
I presume you could optimize your code in terms of allocation by specifying the minimum capacity during the initialization. However, one liner may be the above answer, it's essentially allocation and looping to add 0 in each position.
func gimme_a_new_dictionary(_ inKeyArray:[Int], minCapacity: Int) -> [Int:Int] {
var ret = Dictionray<Int, Int>(minimumCapacity: minCapacity)
for key in inKeyArray {
ret[key] = 0
}
return ret
}
let test1 = gimme_a_new_dictionary([4,6,1,3,0,1000])
Take a look at this official documentation:
/// Use this initializer to avoid intermediate reallocations when you know
/// how many key-value pairs you are adding to a dictionary. The actual
/// capacity of the created dictionary is the smallest power of 2 that
/// is greater than or equal to `minimumCapacity`.
///
/// - Parameter minimumCapacity: The minimum number of key-value pairs to
/// allocate buffer for in the new dictionary.
public init(minimumCapacity: Int)

Iterate through an optional array in Swift?

I'm using the cocoapod SQLite for this project like so
import SQLite
var db = try! Connection()
var id: Expression<Int>!
var identifier: Expression<String>!
With it I am reading a list of moves from a SQLite database.
Every monster has a moves that they can learn. Some monsters can learn more moves than others.
var monster: Monster!
var monArray = [Monster]()
var dataSource: DataSource!
To get the monsters move ID I use this code. This allows me to grab the first move in the array. Changing the 0 would get me the second, third move ect.
monster.moves![0]["move_id"] as! Int
Now I'm using the SQLite database because I need to match monster ID values in my plist with the ones in the SQLite database. I use this code to do so
override func viewWillAppear(_ animated: Bool) {
let movesArray = Array(try! db.prepare(Table("moves").where(identifier == moves.name!.lowercased())))
for user in movesArray {
monArray = dataSource.mons.filter{ $0.moves![0]["move_id"] as! Int == user[id] }
}
}
Everything works fine until I try to increase the index range.
for user in movesArray {
for i in 0...6 {
monArray = dataSource.mons.filter{ $0.moves![i]["move_id"] as! Int == user[id] }
}
}
See where I replace the 0 with the range i? I do that because since monsters have more than one move, if I leave it at 0 my app will only display the monsters that learn that move as their first move. To better explain, my current code does not look through whether the monster knows the move, it only looks through whether the monsters knows the move as its first move.
In the above code I increase the range thinking it would solve my issue, but my app will crash because some monsters only have 1 move in their index, so anything above index 0 will crash with the error
fatal error: Index out of range
So to recap, I need to iterate through the entire array instead of just the first index, without it crashing. How can I achieve this?
Without all the story arround your just asking how to iterate over an array like here
for item in array as type {
...
}
Your question seems to be more of a logical question. If I'm understanding what you have said, then each monster will have a minimum of 1 move, but not guaranteed to have more. So you would need to account for this.
Having a fixed limit like you do will certainly cause problems if not all monsters have that many moves as the array will not always be that size.
I would do something like:
let monsterMoveCount = user.moves.count
for i in 0...monsterMoveCount
// Do whatever logic here
Hopefully this helps!

Realm: live updates of constant values

I'm using SwiftRealm 2.03 and do not understand the magic how constant data (even metadata) gets updated if the data in realm changes...
Here an example:
private func closePastExistingTravelTimes(){
let travelTimes = fetchTravelTimes(onlyNotClosedTravelTimes: true)
guard travelTimes.count > 1 else {
return
}
let numberOfTravelTimes = travelTimes.count
for index in 0..<numberOfTravelTimes-2{
print("index:\(index) count:\(travelTimes.count)")
try! realm.write {
let travelTime = travelTimes[index]
travelTime.travelPhaseIsClosed = true
realm.add(travelTime, update: true)
}
}
}
I'm loading data in the beginning and store them in an constant.
Then I iterate over the items and change the condition of the query so that the fetched data would change if I would query again. But I don't. What even is more suprising that the constant numberOfTravelTimes is even adjusted as you can see below in the log.
index:0 count:5
index:1 count:4
index:2 count:3
index:3 count:2 --> BAM - Exception
What is happening here? How can I be save in my example?
Realm Results objects are live, meaning if you update an object in such a way that it no longer conforms to a Results query, the Results object will update to exclude it. This means you need to be careful when you base a for loop off a Results object, since if the Results object mutates in the middle of the loop, you'll end up with an exception.
Normally, the easiest, but not the most elegant way to mitigate this is to copy all of the Realm objects in a Results object to a static array so it won't mutate during the loop.
In this particular case however, it would be more appropriate to just enclose the entire for loop inside the Realm write transaction. This is generally best practice (Since it's best to batch as many Realm writes into as few write transactions as possible), but in this case, it will have the added advantage of not updating the contents of the Results object until after you're done with it.
try! realm.write { // Open the Realm write transaction outside of the loop
for index in 0..<numberOfTravelTimes-2 {
print("index:\(index) count:\(travelTimes.count)")
let travelTime = travelTimes[index]
travelTime.travelPhaseIsClosed = true
realm.add(travelTime, update: true)
}
}
You should iterate your results in reverse order.
for index in (0 ..<numberOfTravelTimes-1).reverse()
See if it helps

Best way to loop through an array as fast as possible?

Right now, my app retrieves around 1000 CKRecords which gets put into an array. I need to loop through this array later on. When I just had about 100 records to loop through my app worked smoothly and fast, but now it gets slower and slower as the number of records grow. I plan to have around 3000 CKRecords to retrieve so this could be a serious problem for me.
This is what I have come up with for now:
//movies == The NSMutableArray
if let recordMovies = movies.objectEnumerator().allObjects as? [CKRecord] {
for movie in recordMovies {
//Looping
print(movie.objectForKey("Title") as? String)
{
}
But this doesn´t help a lot, seeing as I have to turn the NSMutableArray into an Array of CKRecords. I do this because I have to access the CKRecords attributes when looping, such as movieTitle. What measures can I take to speed up the looping process
Iterating through 1000 (or 5000) elements of an array should take a tiny fraction of a second.
Printing to the debug console is very slow however. Get rid if your print statement.
If movies is an array of CKRecord objects, why can't you just cast it to the desired array type?
let start = NSDate.timeIntervalSinceReferenceDate()
if let recordMovies = movies as? [CKRecord] {
for movie in recordMovies {
//Do whatever work you need to do on each object, but DON'T use print
{
}
let elapsed = NSDate.timeIntervalSinceReferenceDate() - start
print( "iterating through the loop took \(elapsed) seconds")
You can save a lot of time by making "movies" not just an NSMutableArray, but an NSMutableArray of CKRecords, which makes it unnecessary to convert the array from NSMutableArray to a Swift array of CKRecord.
In Objective-C: Instead of
NSMutableArray* movies;
you use
NSMutableArray <CKRecord*> *movies;
As a result, movies [1] for example will have type CKRecord*, not id. An assignment movies [1] = something; requires that "something" is of type CKRecord* or id, nothing else.

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