First I'm going to give a few example structs and show how they chose which properties to encode and decode, then I'll talk a little about extension
s and data flow after
Run this snippet and take a look at the output
import Foundation
/// Conforming to "Codable". Normally, when conforming to a protocol, you have to provide some custom conformance methods
/// If you use Codable as intended, it will *automatically synthesize* the conformance methods. As long as every property
/// that you're asking it to decode is a primitive (string, bool, int, etc), *or* conforms to Codable itself.
/// Notice that everything here is a primitive or conforms to Codable (SomeProperty, MyCodableObject)
struct SomeProperty: Codable {
let metaData: String
let identifier: UUID
}
struct MyCodableObject: Codable {
let id: Int
let title: String
let isNew: Bool
let metaData: [SomeProperty]
var someValue: Int = 3 // because this isn't in coding keys, it won't be decoded
// `CodingKeys` is a special enum that lets the decode us from different key names than our property names
// these are the *only* field names that it looks for.
// `SomeProperty` didn't have coding keys, so it looks for `metaData` and `identifier` as keys
enum CodingKeys: String, CodingKey {
case id // key is called "id"
case title // key is called "title"
case isNew = "is_new" // key is called "is_new" instead of "isNew" like our actual `let`
case metaData = "meta_data_inf"
}
}
/// Look into "multiline strings" in Swift; in my opinion they're super useful for
/// complex console log messages, also for holding test JSON data for API calls
let jsonData = """
{
"id": 3,
"title": "Title",
"is_new": true,
"meta_data_inf": [
{ "metaData": "some extra data", "identifier": "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"},
{ "metaData": "more extra data", "identifier": "F621E1F8-C36C-495A-93FC-0C247A3E6FFF"}
]
}
"""
/// Get that string as utf8 data
let data = jsonData.data(using: .utf8)!
/// Same decode as before
func decodeFromJSON<D: Decodable>(_ data: Data) throws -> D {
try JSONDecoder().decode(D.self, from: data)
}
/// Let's try decoding this JSON
let myObject: MyCodableObject = try decodeFromJSON(data)
print(myObject)
/// `Codable = Encodable & Decodable`, you're actually conforming to TWO separate protocols
/// you can choose to have an object only conform to one, but there's rarely a cost involved
/// in just having that object be `Codable` instead
let reEncodedString: String = String(decoding: try JSONEncoder().encode(myObject), as: UTF8.self)
// Notice that `someValue` wasn't encoded here; not in "CodingKeys!"
print(reEncodedString)
On that line, where I say let myObject: MyCodableObject = try decodeFromJSON(data)
, that generic parameter D
is replaced with MyCodableObject
behind the scenes; decodeFromJSON
gets "specialized" and when I use it at the call site, it's actually calling a method with the signature decodeFromJSON(_ data: Data) throws → MyCodableObject
.
The generic D: Decodable
parameter just means you can use this function on anything Decodable, but when it actually runs on a specific model in code, it's no longer "generic," it's using the actual struct
models you wrote.
Last rant! extension
& computed vars
are your best friends for organizing your code. It means you can split the definition of the model, and the behavior that that model can support. (You can even put them in different files, if your models are huge)
struct SensorData {
var kelvinTemperature: Double
var humidity: Double
}
extension SensorData {
// note that "celsiusTemperature" isn't exactly a "property" of our sensor data,
// however it's deterministic based on the state of `SensorData`, which means
// we should almost certainly put it in a computed variable
var celsiusTemperature: Double { kelvinTemperature - 273 }
// use other computed properties to bootstrap further useful derived behavior
var fahrenheitTemperature: Double { celsiusTemperature * 9 / 5 + 32)
var temperatureIsUnsafe: Bool { fahrenheitTemperature > 220 }
}
All I mean by "data flow" is "how values move through your program." You receive some JSON string from your API, you decode it into a model object, you do some data processing on that model, then you pass it along to your UI to be displayed.
The "rule of thumb" is: "Only let data pass through if it's necessary." For example, if you're using just the MyCodableObject.isNew
property in your UI (displaying some fancy visual banner if isNew == true
), you should only pass that Bool
into your UI-layer: don't pass the whole MyCodableObject
, because it holds a lot of unnecessary extra information (id
, title
, etc).
Don't be afraid of making a lot of custom struct
s and enum
s: this is how you're supposed to use the language!
Look into the concept of "domain modeling." Swift is really good at letting you do good domain modeling without much effort... Have a 5-star rating component? Model the state of it as an enum
with a case
for each possible star. This means that you never have to worry about some edge-case where rating = -1 stars
(if you model rating as an Integer, technically it could hold any integer value...). This example is a little contrived, but hopefully it gets you thinking...