Swift loop over a Dictionary plist with multidimensional array - ios

I have a plist that is set up as shown below:
I'd like to load this into a variable and then loop over each of the items. This is the code I have so far, but to no avail I am seeing the error "Type 'AnyObject' does not conform to protocol 'SequenceType'".
func startTournament(sender: UIBarButtonItem) {
var map: NSDictionary?
if let path = NSBundle.mainBundle().pathForResource("knockout_single_8", ofType: "plist") {
map = NSDictionary(contentsOfFile: path)
}
var matches = NSMutableDictionary()
let rounds = map?["rounds"] as NSArray
for match in rounds[0] { // Error from this line
let mid = match["mid"]
let match = ["names": ["testA", "testB"]]
matches[mid] = match
}
}

The problem you are having is that the foundation classes deal in AnyObjects, but for thinks like for loops that doesn't work:
import Foundation
let o: AnyObject = [1,2,3]
// this won't work, even though o _is_ an array
// error: type 'AnyObject' does not conform to protocol 'SequenceType'
for i in o {
}
// this is perhaps surprising given this does:
o[0] // returns 1 as an AnyObject! (that's a syntactic ! not a surprise/shock ! :)
You may find it easier to convert earlier to Swift types and then use them directly. The downside of this is if you are storing heterogenous data in your plist (i.e. an array that has both strings and integers in it). But it doesn't look you do, so you could write your code like this:
func startTournament() {
if let path = NSBundle.mainBundle().pathForResource("proplist", ofType: "plist") {
if let map = NSDictionary(contentsOfFile: path) {
var matches: [Int:[String:[String]]] = [:]
if let rounds = map["rounds"] as? [[[String:Int]]] {
for match in rounds[0] {
if let mid = match["mid"] {
let match = ["names": ["testA", "testB"]]
matches[mid] = match
}
}
}
}
}
}
Personally, I find this much easier to understand at a glance because the types you are dealing with at each level are easier to see and understand and you're not mashing away with as? and is etc. The big downside to this is if you want to handle multiple different types within the same collection. Then you need to leave things as AnyObject a bit longer, and do more fine-grained as? in the inner loops.
Once you have the above, there are various tricks you can do to handle the optionals better and avoid so many horrid if lets, replace loops with maps and filters etc., but it's probably better to get comfortable with this version first. Also, this code is missing various else clauses that could handle and report failure cases.

Related

Converting a multi dimensional string array into doubles

I have been trying to parse a CSV file forever and I am almost there. I have gotten it to a multi-dimensional array of strings using this code:
let path = Bundle.main.url(forResource: "BaseballSimStats", withExtension: "csv")
var file = String()
do {
file = try String(contentsOf: path!)
print(file)
} catch {
print(error)
}
let stringarray = file.components(separatedBy: "\n").map{ $0.components(separatedBy: ",") }
Now the last step is to turn it into a Double. I am using this code:
probs = Double[[stringarray]]
I get an error saying that the type has no subscript errors. I get rid of the subscript references and the error goes away. Why is this error here and how can I get rid of it? Thanks!
I used .map() to map the String into a Double, this should work for nested array
var strArray = [["1.00000","1.10000"],["2.00000","2.10000"]]
var doubleArray = strArray.map { (arr: Array) -> Array<Any> in
return arr.map({ (value: String) -> Double in
return Double(value)!
})
}
print(strArray)
print(doubleArray)
I am not sure if the double map was needed.
I am not a swift guru but this code should help you achieve what you want..
I'm not familiar with Double[[stringarray]] syntax so I don't know how that's supposed to work. I do know you can't just cast between array types.
The simplest way is probably to wrap the innermost call with Double.init():
file.components(separatedBy: "\n").map{ $0.components(separatedBy: ",").map { Double($0)! }}
Of course, there's a bit more to CSV than just splitting on commas and assuming everything is a valid number, so I'd highly recommend using an existing CSV parsing library for any real data.

`CountedSet` initialization issue

I'm comparing the characters contained within two words. In seeking to accomplish this, Set (aka NSSet) seemed like the way to go to accomplish this task. I've discovered it returns false positives on matches, so I am attempting to use CountedSet (aka NSCountedSet) instead.
I'm able to initialize a Set without issue, but I can't get the CountedSet initializer to work. Here's what I've done...
I start with a String:
// Let's say myTextField.text = "test"
let textFieldCharacters = myTextField.text?.characters
// word is a string from the ENABLE list of words
let wordCharacters = word.characters
Then I dump the characters into an Array:
var wordCharactersArray = [Character]()
for character in wordCharacters {
wordCharacterArray.append(character)
}
var textFieldCharactersArray = [Character]()
for character in textFieldCharacters {
wordCharacterArray.append(character)
}
Then I create a Set from the character arrays:
let textFieldSet = Set<Character>(textFieldCharactersArray)
let wordSet = Set<Character>(wordCharactersArray)
Finally, I test to see if the textFieldSet is a superSet of wordSet with the following:
textFieldSet.isSuperset(of: wordSet)
Going back to my example, if myTextField.text is "test", I'm returning values for word whose characters are a superset of the wordSet, but the counts of the individual elements don't match the character counts of myTextField.text
In researching my issue, I've found CountedSet (fka NSCountedSet), which I think would resolve my issue. It has two method signatures:
public convenience init(array: [AnyObject])
public convenience init(set: Set<NSObject>)
I've tried initializing the 2 sets of characters like so:
let textFieldSet = CountedSet(array: textFieldCharacterArray)
let wordSet = CountedSet(array: wordCharacterArray)
I get the following error for the sets
Cannot convert value of type '[Character]' to expected argument type
'[AnyObject]'.
So I tried initializing the set like this:
let textFieldSet = CountedSet(array: textFieldCharacterArray as! [AnyObject])
Which yields the following error:
'AnyObject' is not a subtype of 'Character'
I've also tried to initialize the CountedSet with a Set, per the method signature, but I get errors when I try to do that, too.
Any suggestions how to initialize a CountedSet would be greatly appreciated.
You are correct that if you need to compare not just the presents of elements but also their count, you should use CountedSet, which is a renaming of NSCountedSet for swift 3.0. The problem you are running into is CountedSet can only accept elements that are objects and Characters are not. As Eric D points out in their comment, the easies way to get around this is by mapping your [Character] to [String] which will bridge to [NSString].
You are not running into this problem using Set, because it is a native Swift collection type that initialize with elements of any type. This is why you can initialize a Set with [Character].
To see the difference:
let word = "helo"
let wordCharacters = Array(word.characters)
let wordSet = Set(wordCharacters)
let wordCharStrings = wordCharacters.map{String($0)}
let wordCountedSet = CountedSet(array: wordCharStrings)
let textField = "hello"
let textFieldCharacters = Array(textField.characters)
let textSet = Set(textFieldCharacters)
let textFieldCharStrings = textFieldCharacters.map{String($0)}
let textFieldCountedSet = CountedSet(array: textFieldCharStrings)
textFieldCountedSet.isSubset(of: wordCountedSet as! Set<NSObject>) // returns false, but if word had two or more l's it would return true
textSet.isSubset(of: wordSet) // returns true

Ambiguous use of subscript?

I can make a Facebook SDK Graph Request to get a user's likes, but I'm having trouble taking the returned values and storing one of the keys in an array of Strings. The request returns an NSDictionary of keys/values. Then, using objectForKey I can get the data key which returns what I want: the id and name of the "liked" page on Facebook.
Data returns elements like this:
{
id = 486379781543416;
name = "Star Wars Movies";
},
I specifically want only the "name" of all of these objects and to throw them into an array [String]. I tried to loop through the objects but I'm getting error ambiguous use of subscript. Here's the relevant code:
request.startWithCompletionHandler{(connection:FBSDKGraphRequestConnection!, result:AnyObject!, error:NSError!) -> Void in
let resultdict = result as! NSDictionary
let likes = resultdict.objectForKey("data") as! NSArray
print("Found \(likes.count) likes")
print(likes)
for object in likes{
let name = object["name"] as! String //error: ambiguous use of subsript
print(name)
}
}
After doing some research it looks like the issue is with the NSArray and that I should instead use Swift data types. I tried casting it to a Swift array but I got different errors.
What's the best way to handle this error?
Thanks!
update: Here is what the facebook API request returns:
{
data = (
{
id = 111276025563005;
name = "Star Wars (film)";
},
{
id = 115061321839188;
name = "Return of the Jedi";
}
);
paging = {
cursors = {
after = MTE1MDYxMzIxODM5MTg4;
before = Mjc0NzYzODk2MTg4NjY5;
};
next = "https://graph.facebook.com/v2.5/10155262562690368/likes?access_token=<redacted>";
};
}
You should always use the native Swift collection types wherever possible as NSArray and NSDictionary are really type-inspecific, and therefore can easily trigger "ambiguous use of subscript" errors.
You'll also want to avoid force down-casting, in case you receive data that's in the wrong format, or no data at all. This situation would be more elegantly handled with a guard, in order to prevent a crash. If your program depends on the force down-casting succeeding, and therefore should crash – then you can always call fatalError in the guard, with a descriptive error message in order to assist you in debugging the problem.
If I understand your data structure correctly, the request returns an AnyObject that should be a [String:AnyObject] (A dictionary of strings to any objects). In the case of the "data" key, the AnyObject value is then a [[String:AnyObject]] (An array of dictionaries of strings to any objects).
Therefore you'll want to do your casting in two stages. First, using a conditional downcast on your result to cast it as a [String:AnyObject]. If this fails, then the else clause of the guard will be executed and the code will return. You'll then want to get out your "data" value (your 'likes' array), and conditionally downcast it to a [[String:AnyObject]]. Both of these statements will handle the possibility of resultDict or resultDict["data"] being nil.
guard let resultDict = result as? [String:AnyObject] else {return}
guard let likes = resultDict["data"] as? [[String:AnyObject]] else {return}
You can put whatever error handling logic you want in the brackets of these statements to handle cases in which the results dictionary doesn't exist, was the wrong format, or there wasn't a 'likes' array in it.
You can then get an array of 'like' names through using flatMap.
let likeNames = likes.flatMap{$0["name"] as? String}
This will create an array of the like names of each dictionary – if the like names don't exist or aren't strings, then they won't be added. Because the compiler knows for certain that likes is a [[String:AnyObject]] – there's no ambiguity in subscripting its elements.
If you want a more general approach such as you're doing in your question, you can use a guard statement within a for loop.
for object in likes {
guard let name = object["name"] as? String else {continue}
print(name)
}
Again, you can put whatever error handling you wish in the brackets of the guard.

iOS 9 JSON Parsing loop

I'm creating an app that should retrieve some JSON from a database.
This is how my JSON looks:
[{"id":"1","longitude":"10","latitude":"10","visibility":"5","timestampAdded":"2015-10-01 15:01:39"},{"id":"2","longitude":"15","latitude":"15","visibility":"5","timestampAdded":"2015-10-01 15:06:25"}]
And this is the code i use:
if let jsonResult = JSON as? Array<Dictionary<String,String>> {
let longitudeValue = jsonResult[0]["longitude"]
let latitudeValue = jsonResult[0]["latitude"]
let visibilityValue = jsonResult[0]["visibility"]
print(longitudeValue!)
print(latitudeValue!)
print(visibilityValue!)
}
As you can see it only gets the first chunk from the JSON and if there are no JSON at all it will crash, but if i want it to count the amount and make an array out of it like this:
var longitudeArray = [10, 15]
var latitudeArray = [10, 15]
And so on...
I also need this to be apple watch compatible so i can't use SwiftyJSON.
What do i do? I really hope you can help me!
Thanks.
SOLVED!
Problems was solved by "Eric D."
This is the code:
do {
if let url = NSURL(string: "YOU URL HERE"),
let data = NSData(contentsOfURL: url),
let jsonResult = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [[String:AnyObject]] {
print(jsonResult)
let longitudeArray = jsonResult.flatMap { $0["longitude"] as? String }
let latitudeArray = jsonResult.flatMap { $0["latitude"] as? String }
print(longitudeArray)
print(latitudeArray)
}
} catch let error as NSError {
print(error.description)
}
Thank you soo much Eric!! :-)
You could use flatMap to get an array of your elements:
let longitudeArray = jsonResult.flatMap { $0["longitude"] as? String }
let latitudeArray = jsonResult.flatMap { $0["latitude"] as? String }
etc.
flatMap is like map but unwraps optionals, which is adequate because we need to safely cast the type of the object we get from each dictionary in the json array.
$0 represents the object in the current iteration of flatMap of the array it's applied to.
If you're currently using SwiftyJSON, then that would be:
let longitudeArray = jsonResult.flatMap { $1["longitude"].string }
let latitudeArray = jsonResult.flatMap { $1["latitude"].string }
because .string is SwiftyJSON's optional String value getter.
But as you said, you don't want to use it (anymore), so you need to use NSJSONSerialization to decode your JSON data, there's plenty of examples on the Web and on SO. Then you will be able to use my original answer.
You're already getting an array with all of the elements (not just the first one. you're simply only accessing the first one). jsonResult is an array of dictionaries. Each dictionary (in this case, based on the json you provided) contains these elements: id, longitude, latitude, visibility and timestampAdded. In order to access each of them, you can simply loop over jsonResult and access the i'th element (and not always the 0 element). This will also prevent the crash you're experiencing with the json is blank or invalid (since you'll only be going over the valid elements in jsonResult.
This will give you the flexibility to create the custom arrays you wish to create (in order to create an array of all of the longitudes, for example, you will simply add that element to the new array while looping over jsonResult). However, if you'd like to save yourself the trouble of manually building these arrays and assuming you have control over the json structure, I would recommend changing the received json to the relevant structure (a dictionary or arrays instead of an array of dictionaries), so it would better fit your needs and provide you the results in the relevant format right "out of the box".

Access a plist in Swift (error: "Expressions are not allowed at the top level")

I have a plist that I would like to access as a Swift Dictionary in Xcode. I'm able to define the contents of the file as an NSDictionary but every time I try to access a specific value/object from it, it shows the error "Expressions are not allowed at the top level". I've tried bracket notation, type casting and class methods (with dot notation) using autocompletion.
My plist contains dictionaries, which contain dictionaries that contain dictionaries with CGFloats and Strings.
I would like to access the instance's values.
Here's the code I use to define it, which is at the beginning of my GameScene.swift (it's a SpriteKit game):
let levelBlocks = NSDictionary(contentsOfFile: NSBundle.mainBundle().pathForResource("LevelBlocks", ofType: "plist"))
I would like to be able to do:
levelBlocks["Level1"]
EDIT: Added more information.
I've also tried putting it in the ViewDidLoad(), init() and using a generic that conforms to Hashable inside of a function instead of AnyObject/Any. I still can't access the String.
EDIT 2: I've tried this in the ViewDidLoad(), it returns the error 254:
let levelBlocks = NSDictionary(contentsOfFile: NSBundle.mainBundle().pathForResource("LevelBlocks", ofType: "plist")) as Dictionary<String, Dictionary<String, Dictionary<String, String>>>
let test = levelBlocks["Level1"] as Dictionary<String, Dictionary<String, String>>
let test1 = test["Block0"] as Dictionary<String, String>
let test2 = test1["color"] as String
println(test2)
Here's the solution I found:
let levelBlocks = NSDictionary(contentsOfFile: NSBundle.mainBundle().pathForResource("LevelBlocks", ofType: "plist"))
let test: AnyObject = levelBlocks.objectForKey("Level1")
println(test) // Prints the value of test
I set the type of test to AnyObject to silence a warning about an unexpected inference that could occur.
Also, it has to be done in a class method.
To access and save a specific value of a known type:
let value = levelBlocks.objectForKey("Level1").objectForKey("amount") as Int
println(toString(value)) // Converts value to String and prints it
Xcode gives that error when you have an expression outside of a class or instance method. Declaring a global variable like levelBlocks is okay, but trying to call methods on a variable (which is what subscripting does) is an expression and not allowed there. If you're just testing things out, you can create another global:
let levelBlocks = NSDictionary(contentsOfFile: NSBundle.mainBundle().pathForResource("LevelBlocks", ofType: "plist"))
let level1 = levelBlocks["level1"]
but really any kind of manipulation should happen inside your class.
Note: This applies to ".swift" files -- playgrounds are built just for this sort of informal experimentation. Does your attempted code work in a playground?
Here's a breakdown of a Swift file that contains a class with the different levels scope shown in comments:
// MyViewController.swift
let globalNumber = 10 // global scope: can be accessed from
// anywhere in this module.
class MyViewController : ViewController {
var instanceNumber = 5 // instance scope: instances of this class each
// maintain their own value. should be
// accessed within methods using self.varname
override func viewDidLoad() {
super.viewDidLoad()
var localNumber = self.instanceNumber * globalNumber
// local scope: will disappear after this method
// finishes executing (in most cases)
}
}
In your case, you'll want to load the plist and access its values inside one of your class, probably the init or a ...DidLoad method.
As for pulling items out of the dictionary, keep in mind that an NSDictionary is bridged directly to Swift as a Dictionary, so you can load and parse it like this:
let path = NSBundle.mainBundle().pathForResource("LevelBlocks", ofType: "plist")
if let levelBlocks = NSDictionary(contentsOfFile: path) as? Dictionary<String, AnyObject> {
let level1 = levelBlocks["level1"] // level1 is of type AnyObject?
let level2 = levelBlocks["level2"] as NSString // type NSString
let level3: String = levelBlocks["level3"] as NSString // type String
}

Resources