Remember optionals.
Let's start with answering your primary concern: what do I do with the summary
field?
You've already appropriately set up a constructor which takes a String?
argument for summary
. But your particular code never takes advantage of this fact, does it?
if let summary = curr_dict["summary"] as? String
Let's break down what's going on here piece by piece.
curr_dict["summary"]
returns an AnyObject?
. It makes no assumptions about the type, since curr_dict
is an NSDictionary
. It either returns whatever value is at that key, or nil
if the key doesn't exist.
as? String
tries to cast that value as a String
. If the value was nil
or if it can't be cast as a string, it returns nil
, otherwise it returns String
.
let summary =
declares the variable called summary
, whose type is determined by the return of everything to the right. Since the top of the right-side's stack is as? String
, which can return nil
or a String
, then the return type is String?
.
if let
casts an optional type (String?
) as a non-optional type (String
) if it can, otherwise it enters the else
block.
So... we can get rid of the if let
part, and just make ourselves an optional string, right?
let summary = curr_dict["summary"] as? String
return WeatherForecast(date: date, summary: summary, latitude: latitude, longitude: longitude)
But wait, there's more!
guard let dict = json as? NSDictionary, // ...
We should prefer to use Swift types over Objective-C types when that makes sense. And here is a place where it does. What we want is a [String:Any]
.
guard let dictionary = json as? [String:Any], // ...
Encapsulate.
We should also prefer encapsulation. It doesn't make particular sense to have a latitude
and longitude
property. What we need is a type that encapsulates these, as they're just two parts of describing one thing: location.
The easiest solution would be simply importing CoreLocation
and either using CLLocationCooridinate2D
or CLLocation
. The downside to this, however, is that any class that uses our struct has to import CoreLocation
in order to use this property, but we can easily get around this with a typealias
:
typealias WeatherForecastLocation = CLLocationCoordinate2D
And now we replace our latitude
and longitude
properties with a property of type WeatherForecastLocation
.
Informative Errors
So, JSONErrorType.InvalidJSON
... how useful is this? It's an enum with a single case, just so we have something to throw. We can be so much more informative.
First of all, two types of valid top-level JSON objects. Dictionary or array. So any method we write to parse JSON will expect one of these two types, so the first error will be the JSON not being that type. That gives use two errors right off the bat (and let's grab some better names here):
enum JSONParseError: ErrorType {
case ExpectedArrayNotFound
case ExpectedDictionaryNotFound
}
If we are looking for JSON with a top-level object of dictionary, and we don't find it, we throw ExpectedDictionaryNotFound
. It doesn't matter what it looked like. But this is already more informative... we couldn't even manage to get past the first level.
Now, what's another sort of JSON parsing error? Missing keys. But we don't want to simply be that generic. Let's specify what key is missing, shall we? Swift enums can have associated values. With our MissingKey
error, we should use a String
associated value to identify the missing key. Or enum
now looks like this:
enum JSONParseError: ErrorType {
case ExpectedArrayNotFound
case ExpectedDictionaryNotFound
case MissingKey(String)
}
We also have an error case for when the type of the value doesn't match what we expect. For this, we want to specify the key that's problematic and specify the type that we expect it to be, so let's associate a tuple of two strings:
enum JSONParseError: ErrorType {
case ExpectedArrayNotFound
case ExpectedDictionaryNotFound
case MissingKey(String)
case InvalidTypeForKey(String,String)
}
So, the first string specifies the key, and the second string is a message something like "Expected Int"
, "Expected Array"
.
Now... the last thing we can do here that's probably quite helpful... return the JSON that was passed in with error.
enum JSONParseError: ErrorType {
case ExpectedArrayNotFound(Any)
case ExpectedDictionaryNotFound(Any)
case MissingKey(String,Any)
case InvalidTypeForKey(String,String,Any)
}