├── .github
└── FUNDING.yml
├── .travis.yml
├── Chapter 1
└── Plane.playground
│ ├── Contents.swift
│ ├── Sources
│ └── Plane.swift
│ └── contents.xcplayground
├── Chapter 2
└── Flight Plan.playground
│ ├── Contents.swift
│ ├── Sources
│ ├── Aircraft.swift
│ ├── FlightPlan.swift
│ └── FlightRules.swift
│ └── contents.xcplayground
├── Chapter 3
├── AnyDecodable.playground
│ ├── Contents.swift
│ ├── Sources
│ │ ├── AnyDecodable.swift
│ │ └── Report.swift
│ └── contents.xcplayground
├── Coordinates.playground
│ ├── Contents.swift
│ ├── Sources
│ │ └── Coordinates.swift
│ └── contents.xcplayground
├── EconomySeat.playground
│ ├── Contents.swift
│ ├── Sources
│ │ ├── EconomySeat.swift
│ │ └── PremiumEconomySeat.swift
│ └── contents.xcplayground
├── EitherBirdOrPlane.playground
│ ├── Contents.swift
│ ├── Sources
│ │ ├── Bird.swift
│ │ ├── Either.swift
│ │ └── Plane.swift
│ └── contents.xcplayground
├── FuelPrice.playground
│ ├── Contents.swift
│ ├── Sources
│ │ ├── AmericanFuelPrice.swift
│ │ ├── CanadianFuelPrice.swift
│ │ ├── Fuel.swift
│ │ └── FuelPrice.swift
│ └── contents.xcplayground
├── Pixel.playground
│ ├── Contents.swift
│ ├── Sources
│ │ ├── ColorEncodingStrategy.swift
│ │ └── Pixel.swift
│ └── contents.xcplayground
└── Route.playground
│ ├── Contents.swift
│ ├── Sources
│ └── Route.swift
│ └── contents.xcplayground
├── Chapter 4
└── Music Store.playground
│ ├── Contents.swift
│ ├── Sources
│ ├── AppleiTunesSearchURLComponents.swift
│ ├── Explicitness.swift
│ ├── MasterViewController.swift
│ ├── SearchResponse.swift
│ └── SearchResult.swift
│ └── contents.xcplayground
├── Chapter 5
└── In Flight Service.playground
│ ├── Contents.swift
│ ├── Resources
│ └── Inventory.plist
│ ├── Sources
│ ├── Item.swift
│ ├── LineItemView.swift
│ ├── Order.swift
│ ├── OrderSelectionViewController.swift
│ └── OrdersViewController.swift
│ └── contents.xcplayground
├── Chapter 6
└── Luggage Scanner.playground
│ ├── Contents.swift
│ ├── Resources
│ ├── BaggageCheck.mom
│ ├── BaggageCheck.xcdatamodeld
│ │ ├── .xccurrentversion
│ │ └── CoreNothing.xcdatamodel
│ │ │ └── contents
│ └── tag.png
│ ├── Sources
│ ├── CodingUserInfoKey+Context.swift
│ ├── Luggage.swift
│ ├── LuggageTagScanner.swift
│ ├── Passenger.swift
│ ├── Support.swift
│ └── ViewController.swift
│ └── contents.xcplayground
├── Chapter 7
└── MessagePackEncoder.playground
│ ├── Contents.swift
│ ├── Sources
│ ├── KeyedValueContainer.swift
│ ├── MessagePackEncoder.swift
│ ├── Numeric+Bytes.swift
│ ├── SingleValueContainer.swift
│ ├── UnkeyedValueContainer.swift
│ └── _MessagePackEncoder.swift
│ └── contents.xcplayground
├── LICENSE.md
├── README.md
└── cover.jpg
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [mattt]
2 | custom: https://flight.school/books/codable
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: swift
2 | osx_image: xcode10.2
3 | env:
4 | global:
5 | - SDK=iphoneos
6 | - TARGET=armv7-apple-ios10
7 | matrix:
8 | - PLAYGROUND_DIR="Chapter 1/Plane.playground"
9 | - PLAYGROUND_DIR="Chapter 2/Flight Plan.playground"
10 | - PLAYGROUND_DIR="Chapter 3/AnyDecodable.playground"
11 | - PLAYGROUND_DIR="Chapter 3/Coordinates.playground"
12 | - PLAYGROUND_DIR="Chapter 3/EconomySeat.playground"
13 | - PLAYGROUND_DIR="Chapter 3/EitherBirdOrPlane.playground"
14 | - PLAYGROUND_DIR="Chapter 3/FuelPrice.playground"
15 | - PLAYGROUND_DIR="Chapter 3/Pixel.playground"
16 | - PLAYGROUND_DIR="Chapter 3/Route.playground"
17 | - PLAYGROUND_DIR="Chapter 4/Music Store.playground"
18 | - PLAYGROUND_DIR="Chapter 5/In Flight Service.playground"
19 | - PLAYGROUND_DIR="Chapter 6/Luggage Scanner.playground"
20 | - PLAYGROUND_DIR="Chapter 7/MessagePackEncoder.playground"
21 | script:
22 | xcrun swift --version &&
23 | cd "${PLAYGROUND_DIR}" &&
24 | xcrun -sdk "${SDK}"
25 | swiftc -target "${TARGET}"
26 | -emit-library -emit-module -module-name AuxiliarySources
27 | Sources/*.swift &&
28 | if ! xcrun swiftc -emit-imported-modules Contents.swift |
29 | grep -q "PlaygroundSupport";
30 | then
31 | cat <(echo "import AuxiliarySources") Contents.swift > main.swift &&
32 | xcrun -sdk "${SDK}"
33 | swiftc -target "${TARGET}"
34 | -I "." -L "." -lAuxiliarySources -module-link-name AuxiliarySources
35 | -o Playground main.swift;
36 | fi
37 |
--------------------------------------------------------------------------------
/Chapter 1/Plane.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let json = """
4 | {
5 | "manufacturer": "Cessna",
6 | "model": "172 Skyhawk",
7 | "seats": 4,
8 | }
9 | """.data(using: .utf8)!
10 |
11 | let decoder = JSONDecoder()
12 | let plane = try! decoder.decode(Plane.self, from: json)
13 |
14 | print(plane.manufacturer)
15 | print(plane.model)
16 | print(plane.seats)
17 |
18 | let encoder = JSONEncoder()
19 | let reencodedJSON = try! encoder.encode(plane)
20 |
21 | print(String(data: reencodedJSON, encoding: .utf8)!)
22 |
--------------------------------------------------------------------------------
/Chapter 1/Plane.playground/Sources/Plane.swift:
--------------------------------------------------------------------------------
1 | public struct Plane: Codable {
2 | public var manufacturer: String
3 | public var model: String
4 | public var seats: Int
5 |
6 | /* Conformance to Decodable and Encodable is automatically synthesized,
7 | so this code isn't necessary.
8 | */
9 |
10 | /*
11 | private enum CodingKeys: String, CodingKey {
12 | case manufacturer
13 | case model
14 | case seats
15 | }
16 |
17 | public init(from decoder: Decoder) throws {
18 | let container = try decoder.container(keyedBy: CodingKeys.self)
19 | self.manufacturer = try container.decode(String.self, forKey: .manufacturer)
20 | self.model = try container.decode(String.self, forKey: .model)
21 | self.seats = try container.decode(Int.self, forKey: .seats)
22 | }
23 |
24 | public func encode(to encoder: Encoder) throws {
25 | var container = encoder.container(keyedBy: CodingKeys.self)
26 | try container.encode(self.manufacturer, forKey: .manufacturer)
27 | try container.encode(self.model, forKey: .model)
28 | try container.encode(self.seats, forKey: .seats)
29 | }
30 | */
31 | }
32 |
--------------------------------------------------------------------------------
/Chapter 1/Plane.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter 2/Flight Plan.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let json = """
4 | {
5 | "aircraft": {
6 | "identification": "NA12345",
7 | "color": "Blue/White"
8 | },
9 | "route": ["KTTD", "KHIO"],
10 | "departure_time": {
11 | "proposed": "2018-04-20T15:07:24-07:00",
12 | "actual": "2018-04-20T15:07:24-07:00"
13 | },
14 | "flight_rules": "IFR",
15 | "remarks": null
16 | }
17 | """.data(using: .utf8)!
18 |
19 | var decoder = JSONDecoder()
20 | decoder.dateDecodingStrategy = .iso8601
21 |
22 | let plan = try! decoder.decode(FlightPlan.self, from: json)
23 | print(plan.aircraft.identification)
24 | print(plan.actualDepartureDate!)
25 |
--------------------------------------------------------------------------------
/Chapter 2/Flight Plan.playground/Sources/Aircraft.swift:
--------------------------------------------------------------------------------
1 | public struct Aircraft: Codable {
2 | public var identification: String
3 | public var color: String
4 | }
5 |
--------------------------------------------------------------------------------
/Chapter 2/Flight Plan.playground/Sources/FlightPlan.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct FlightPlan: Codable {
4 | public var aircraft: Aircraft
5 |
6 | public var route: [String]
7 |
8 | public var flightRules: FlightRules
9 |
10 | private var departureDates: [String: Date]
11 |
12 | public var proposedDepartureDate: Date? {
13 | return departureDates["proposed"]
14 | }
15 |
16 | public var actualDepartureDate: Date? {
17 | return departureDates["actual"]
18 | }
19 |
20 | public var remarks: String?
21 |
22 | private enum CodingKeys: String, CodingKey {
23 | case aircraft
24 | case flightRules = "flight_rules"
25 | case route
26 | case departureDates = "departure_time"
27 | case remarks
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Chapter 2/Flight Plan.playground/Sources/FlightRules.swift:
--------------------------------------------------------------------------------
1 | public enum FlightRules: String, Codable {
2 | case visual = "VFR"
3 | case instrument = "IFR"
4 | }
5 |
--------------------------------------------------------------------------------
/Chapter 2/Flight Plan.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter 3/AnyDecodable.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let json = """
4 | {
5 | "title": "Sint Pariatur Enim ut Lorem Eiusmod",
6 | "body": "Cillum deserunt ullamco minim nulla et nulla ea ex eiusmod ea exercitation qui irure irure. Ut laboris amet Lorem deserunt consequat irure dolore quis elit eiusmod. Dolore duis velit consequat dolore. Qui aliquip ad id eiusmod in do officia. Non fugiat esse laborum enim pariatur cillum. Minim aliquip minim exercitation anim adipisicing amet. Culpa proident adipisicing labore enim ullamco veniam.",
7 | "metadata": {
8 | "key": "value"
9 | }
10 | }
11 | """.data(using: .utf8)!
12 |
13 | let decoder = JSONDecoder()
14 | let report = try decoder.decode(Report.self, from: json)
15 |
16 | print(report.title)
17 | print(report.body)
18 | print(report.metadata["key"])
19 |
--------------------------------------------------------------------------------
/Chapter 3/AnyDecodable.playground/Sources/AnyDecodable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct AnyDecodable: Decodable {
4 | public let value: Any
5 |
6 | public init(_ value: Any?) {
7 | self.value = value ?? ()
8 | }
9 |
10 | public init(from decoder: Decoder) throws {
11 | let container = try decoder.singleValueContainer()
12 |
13 | if container.decodeNil() {
14 | self.value = ()
15 | } else if let bool = try? container.decode(Bool.self) {
16 | self.value = bool
17 | } else if let int = try? container.decode(Int.self) {
18 | self.value = int
19 | } else if let uint = try? container.decode(UInt.self) {
20 | self.value = uint
21 | } else if let double = try? container.decode(Double.self) {
22 | self.value = double
23 | } else if let string = try? container.decode(String.self) {
24 | self.value = string
25 | } else if let array = try? container.decode([AnyDecodable].self) {
26 | self.value = array.map { $0.value }
27 | } else if let dictionary = try? container.decode([String: AnyDecodable].self) {
28 | self.value = dictionary.mapValues { $0.value }
29 | } else {
30 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyCodable value cannot be decoded")
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Chapter 3/AnyDecodable.playground/Sources/Report.swift:
--------------------------------------------------------------------------------
1 | public struct Report: Decodable {
2 | public var title: String
3 | public var body: String
4 | public var metadata: [String: AnyDecodable]
5 | }
6 |
--------------------------------------------------------------------------------
/Chapter 3/AnyDecodable.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter 3/Coordinates.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let json = """
4 | {
5 | "coordinates": [
6 | {
7 | "latitude": 37.332,
8 | "longitude": -122.011
9 | },
10 | [-122.011, 37.332],
11 | "37.332, -122.011"
12 | ]
13 | }
14 | """.data(using: .utf8)!
15 |
16 | let decoder = JSONDecoder()
17 |
18 | let coordinates = try! decoder.decode([String: [Coordinate]].self, from: json)["coordinates"]
19 | print(coordinates!)
20 |
--------------------------------------------------------------------------------
/Chapter 3/Coordinates.playground/Sources/Coordinates.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct Coordinate: Decodable {
4 | public var latitude: Double
5 | public var longitude: Double
6 | public var elevation: Double?
7 |
8 | private enum CodingKeys: String, CodingKey {
9 | case latitude
10 | case longitude
11 | case elevation
12 | }
13 |
14 | public init(from decoder: Decoder) throws {
15 | if let container =
16 | try? decoder.container(keyedBy: CodingKeys.self)
17 | {
18 | self.latitude =
19 | try container.decode(Double.self, forKey: .latitude)
20 | self.longitude =
21 | try container.decode(Double.self, forKey: .longitude)
22 | self.elevation =
23 | try container.decodeIfPresent(Double.self,
24 | forKey: .elevation)
25 | } else if var container = try? decoder.unkeyedContainer() {
26 | self.longitude = try container.decode(Double.self)
27 | self.latitude = try container.decode(Double.self)
28 | self.elevation = try container.decodeIfPresent(Double.self)
29 | } else if let container = try? decoder.singleValueContainer() {
30 | let string = try container.decode(String.self)
31 |
32 | let scanner = Scanner(string: string)
33 | scanner.charactersToBeSkipped = CharacterSet(charactersIn: "[ ,]")
34 |
35 | var longitude = Double()
36 | var latitude = Double()
37 |
38 |
39 | guard scanner.scanDouble(&longitude),
40 | scanner.scanDouble(&latitude)
41 | else {
42 | throw DecodingError.dataCorruptedError(
43 | in: container,
44 | debugDescription: "Invalid coordinate string"
45 | )
46 | }
47 |
48 | self.latitude = latitude
49 | self.longitude = longitude
50 | self.elevation = nil
51 | } else {
52 | let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to decode Coordinate")
53 | throw DecodingError.dataCorrupted(context)
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Chapter 3/Coordinates.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter 3/EconomySeat.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let json = """
4 | {
5 | "number": 7,
6 | "letter": "A",
7 | "mealPreference": "vegetarian"
8 | }
9 | """.data(using: .utf8)!
10 |
11 | let decoder = JSONDecoder()
12 | let seat = try! decoder.decode(PremiumEconomySeat.self, from: json)
13 |
14 | print(seat.number)
15 | print(seat.letter)
16 | print(seat.mealPreference!)
17 |
--------------------------------------------------------------------------------
/Chapter 3/EconomySeat.playground/Sources/EconomySeat.swift:
--------------------------------------------------------------------------------
1 | public class EconomySeat: Decodable {
2 | public var number: Int
3 | public var letter: String
4 |
5 | public init(number: Int, letter: String) {
6 | self.number = number
7 | self.letter = letter
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Chapter 3/EconomySeat.playground/Sources/PremiumEconomySeat.swift:
--------------------------------------------------------------------------------
1 | public class PremiumEconomySeat: EconomySeat {
2 | public var mealPreference: String?
3 |
4 | private enum CodingKeys: String, CodingKey {
5 | case mealPreference
6 | }
7 |
8 | public init(number: Int, letter: String, mealPreference: String?) {
9 | super.init(number: number, letter: letter)
10 | self.mealPreference = mealPreference
11 | }
12 |
13 | public required init(from decoder: Decoder) throws {
14 | let container = try decoder.container(keyedBy: CodingKeys.self)
15 | self.mealPreference =
16 | try container.decodeIfPresent(String.self, forKey: .mealPreference)
17 | try super.init(from: decoder)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Chapter 3/EconomySeat.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter 3/EitherBirdOrPlane.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let json = """
4 | [
5 | {
6 | "type": "bird",
7 | "genus": "Chaetura",
8 | "species": "Vauxi"
9 | },
10 | {
11 | "type": "plane",
12 | "identifier": "NA12345"
13 | }
14 | ]
15 | """.data(using: .utf8)!
16 |
17 | let decoder = JSONDecoder()
18 | let objects = try! decoder.decode([Either].self, from: json)
19 |
20 | for object in objects {
21 | switch object {
22 | case .left(let bird):
23 | print("Poo-tee-weet? It's \(bird.genus) \(bird.species)!")
24 | case .right(let plane):
25 | print("Vooooooooooooooom! It's \(plane.identifier)!")
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Chapter 3/EitherBirdOrPlane.playground/Sources/Bird.swift:
--------------------------------------------------------------------------------
1 | public struct Bird: Decodable {
2 | public var genus: String
3 | public var species: String
4 | }
5 |
--------------------------------------------------------------------------------
/Chapter 3/EitherBirdOrPlane.playground/Sources/Either.swift:
--------------------------------------------------------------------------------
1 | public enum Either {
2 | case left(T)
3 | case right(U)
4 | }
5 |
6 | extension Either: Decodable where T: Decodable, U: Decodable {
7 | public init(from decoder: Decoder) throws {
8 | if let value = try? T(from: decoder) {
9 | self = .left(value)
10 | } else if let value = try? U(from: decoder) {
11 | self = .right(value)
12 | } else {
13 | let context = DecodingError.Context(
14 | codingPath: decoder.codingPath,
15 | debugDescription:
16 | "Cannot decode \(T.self) or \(U.self)"
17 | )
18 | throw DecodingError.dataCorrupted(context)
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Chapter 3/EitherBirdOrPlane.playground/Sources/Plane.swift:
--------------------------------------------------------------------------------
1 | public struct Plane: Decodable {
2 | public var identifier: String
3 | }
4 |
--------------------------------------------------------------------------------
/Chapter 3/EitherBirdOrPlane.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter 3/FuelPrice.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let americanJSON = """
4 | [
5 | {
6 | "fuel": "100LL",
7 | "price": 5.60
8 | },
9 | {
10 | "fuel": "Jet A",
11 | "price": 4.10
12 | }
13 | ]
14 | """.data(using: .utf8)!
15 |
16 | let canadianJSON = """
17 | {
18 | "fuels": [
19 | {
20 | "type": "100LL",
21 | "price": 2.54
22 | },
23 | {
24 | "type": "Jet A",
25 | "price": 3.14,
26 | },
27 | {
28 | "type": "Jet B",
29 | "price": 3.03
30 | }
31 | ]
32 | }
33 | """.data(using: .utf8)!
34 |
35 | let decoder = JSONDecoder()
36 | let usPrices = try! decoder.decode([AmericanFuelPrice].self, from: americanJSON)
37 | let caPrices = try! decoder.decode([String: [CanadianFuelPrice]].self, from: canadianJSON)
38 |
39 | var prices: [FuelPrice] = []
40 | prices.append(contentsOf: usPrices)
41 | prices.append(contentsOf: caPrices["fuels"]! as [FuelPrice])
42 |
43 | print(prices)
44 |
--------------------------------------------------------------------------------
/Chapter 3/FuelPrice.playground/Sources/AmericanFuelPrice.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct AmericanFuelPrice: Decodable {
4 | public let fuel: Fuel
5 |
6 | /// USD / gallon
7 | public let price: Decimal
8 | }
9 |
10 | extension AmericanFuelPrice: FuelPrice {
11 | public var type: Fuel {
12 | return self.fuel
13 | }
14 |
15 | public var pricePerLiter: Decimal {
16 | return self.price /
17 | 3.78541
18 | }
19 |
20 | public var currency: String {
21 | return "USD"
22 | }
23 | }
24 |
25 | extension AmericanFuelPrice: CustomStringConvertible {}
26 |
--------------------------------------------------------------------------------
/Chapter 3/FuelPrice.playground/Sources/CanadianFuelPrice.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct CanadianFuelPrice: Decodable {
4 | public let type: Fuel
5 |
6 | /// CAD / liter
7 | public let price: Decimal
8 | }
9 |
10 | extension CanadianFuelPrice: FuelPrice {
11 | public var pricePerLiter: Decimal {
12 | return self.price
13 | }
14 |
15 | public var currency: String {
16 | return "CAD"
17 | }
18 | }
19 |
20 | extension CanadianFuelPrice: CustomStringConvertible {}
21 |
--------------------------------------------------------------------------------
/Chapter 3/FuelPrice.playground/Sources/Fuel.swift:
--------------------------------------------------------------------------------
1 | public enum Fuel: String, Decodable {
2 | case jetA = "Jet A"
3 | case jetB = "Jet B"
4 | case oneHundredLowLead = "100LL"
5 | }
6 |
7 | extension Fuel: CustomStringConvertible {
8 | public var description: String {
9 | return self.rawValue
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Chapter 3/FuelPrice.playground/Sources/FuelPrice.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol FuelPrice {
4 | var type: Fuel { get }
5 | var pricePerLiter: Decimal { get }
6 | var currency: String { get }
7 | }
8 |
9 | extension FuelPrice {
10 | public var description: String {
11 | let formatter = NumberFormatter()
12 | formatter.numberStyle = .currencyISOCode
13 | formatter.currencyCode = self.currency
14 |
15 | return "\(type.rawValue): \(formatter.string(from: self.pricePerLiter as NSNumber)!)"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Chapter 3/FuelPrice.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter 3/Pixel.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let encoder = JSONEncoder()
4 | encoder.userInfo[.colorEncodingStrategy] = ColorEncodingStrategy.hexadecimal(hash: true)
5 |
6 | let cyan = Pixel(red: 0, green: 255, blue: 255)
7 | let magenta = Pixel(red: 255, green: 0, blue: 255)
8 | let yellow = Pixel(red: 255, green: 255, blue: 0)
9 | let black = Pixel(red: 0, green: 0, blue: 0)
10 |
11 | let json = try! encoder.encode([cyan, magenta, yellow, black])
12 | let string = String(data: json, encoding: .utf8)!
13 |
14 | print(string)
15 |
--------------------------------------------------------------------------------
/Chapter 3/Pixel.playground/Sources/ColorEncodingStrategy.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum ColorEncodingStrategy {
4 | case rgb
5 | case hexadecimal(hash: Bool)
6 | }
7 |
8 | extension CodingUserInfoKey {
9 | public static let colorEncodingStrategy =
10 | CodingUserInfoKey(rawValue: "colorEncodingStrategy")!
11 | }
12 |
--------------------------------------------------------------------------------
/Chapter 3/Pixel.playground/Sources/Pixel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct Pixel {
4 | public var red: UInt8
5 | public var green: UInt8
6 | public var blue: UInt8
7 |
8 | public init(red: UInt8, green: UInt8, blue: UInt8) {
9 | self.red = red
10 | self.green = green
11 | self.blue = blue
12 | }
13 | }
14 |
15 | extension Pixel: Encodable {
16 | public func encode(to encoder: Encoder) throws {
17 | var container = encoder.singleValueContainer()
18 |
19 | switch encoder.userInfo[.colorEncodingStrategy]
20 | as? ColorEncodingStrategy
21 | {
22 | case let .hexadecimal(hash)?:
23 | try container.encode(
24 | (hash ? "#" : "") +
25 | String(format: "%02X%02X%02X", red, green, blue)
26 | )
27 | default:
28 | try container.encode(
29 | String(format: "rgb(%d, %d, %d)", red, green, blue)
30 | )
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Chapter 3/Pixel.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter 3/Route.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let json = """
4 | {
5 | "points": ["KSQL", "KWVI"],
6 | "KSQL": {
7 | "code": "KSQL",
8 | "name": "San Carlos Airport"
9 | },
10 | "KWVI": {
11 | "code": "KWVI",
12 | "name": "Watsonville Municipal Airport"
13 | }
14 | }
15 | """.data(using: .utf8)!
16 |
17 | let decoder = JSONDecoder()
18 | let route = try decoder.decode(Route.self, from: json)
19 | print(route.points.map{ $0.code })
20 |
--------------------------------------------------------------------------------
/Chapter 3/Route.playground/Sources/Route.swift:
--------------------------------------------------------------------------------
1 | public struct Route: Decodable {
2 | public struct Airport: Decodable {
3 | public var code: String
4 | public var name: String
5 | }
6 |
7 | public var points: [Airport]
8 |
9 | private struct CodingKeys: CodingKey {
10 | var stringValue: String
11 |
12 | var intValue: Int? {
13 | return nil
14 | }
15 |
16 | init?(stringValue: String) {
17 | self.stringValue = stringValue
18 | }
19 |
20 | init?(intValue: Int) {
21 | return nil
22 | }
23 |
24 | static let points =
25 | CodingKeys(stringValue: "points")!
26 | }
27 |
28 | public init(from coder: Decoder) throws {
29 | let container = try coder.container(keyedBy: CodingKeys.self)
30 |
31 | var points: [Airport] = []
32 | let codes = try container.decode([String].self, forKey: .points)
33 | for code in codes {
34 | let key = CodingKeys(stringValue: code)!
35 | let airport = try container.decode(Airport.self, forKey: key)
36 | points.append(airport)
37 | }
38 | self.points = points
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Chapter 3/Route.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter 4/Music Store.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 | import PlaygroundSupport
4 |
5 | let viewController = MasterViewController()
6 | let navigationController = UINavigationController(rootViewController: viewController)
7 |
8 | viewController.search(for: Music.self, with: <#artist#>)
9 |
10 | PlaygroundPage.current.needsIndefiniteExecution = true
11 | PlaygroundPage.current.liveView = navigationController
12 |
--------------------------------------------------------------------------------
/Chapter 4/Music Store.playground/Sources/AppleiTunesSearchURLComponents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol MediaType {
4 | associatedtype Entity: RawRepresentable where Entity.RawValue == String
5 | associatedtype Attribute: RawRepresentable where Attribute.RawValue == String
6 | }
7 |
8 | public enum Movie: MediaType {
9 | public enum Entity: String {
10 | case movie
11 | case artist = "movieArtist"
12 | }
13 |
14 | public enum Attribute: String {
15 | case actor = "actorTerm"
16 | case genre = "genreIndex"
17 | case artist = "artistTerm"
18 | case shortFilm = "shortFilmTerm"
19 | case producer = "producerTerm"
20 | case ratingTerm = "ratingTerm"
21 | case director = "directorTerm"
22 | case releaseYear = "releaseYearTerm"
23 | case featureFilm = "featureFilmTerm"
24 | case movieArtist = "movieArtistTerm"
25 | case movie = "movieTerm"
26 | case ratingIndex = "ratingIndex"
27 | case description = "descriptionTerm"
28 | }
29 | }
30 |
31 | public struct Podcast: MediaType {
32 | public enum Entity: String {
33 | case podcast
34 | case author = "podcastAuthor"
35 | }
36 |
37 | public enum Attribute: String {
38 | case title = "titleTerm"
39 | case language = "languageTerm"
40 | case author = "authorTerm"
41 | case genre = "genreIndex"
42 | case artist = "artistTerm"
43 | case rating = "ratingIndex"
44 | case keywords = "keywordsTerm"
45 | case description = "descriptionTerm"
46 | }
47 | }
48 |
49 | public struct Music: MediaType {
50 | public enum Entity: String {
51 | case artist = "musicArtist"
52 | case track = "musicTrack"
53 | case album
54 | case musicVideo
55 | case mix
56 | case song
57 | }
58 |
59 | public enum Attribute: String {
60 | case mix = "mixTerm"
61 | case genre = "genreIndex"
62 | case artist = "artistTerm"
63 | case composer = "composerTerm"
64 | case album = "albumTerm"
65 | case rating = "ratingIndex"
66 | case song = "songTerm"
67 | }
68 | }
69 |
70 | public struct MusicVideo: MediaType {
71 | public enum Entity: String {
72 | case musicVideo
73 | case artist = "musicArtist"
74 | }
75 |
76 | public enum Attribute: String {
77 | case genre = "genreIndex"
78 | case artist = "artistTerm"
79 | case album = "albumTerm"
80 | case rating = "ratingIndex"
81 | case song = "songTerm"
82 | }
83 | }
84 |
85 | public struct AudioBook: MediaType {
86 | public enum Entity: String {
87 | case audiobook
88 | case author = "audiobookAuthor"
89 | }
90 |
91 | public enum Attribute: String {
92 | case title = "titleTerm"
93 | case author = "authorTerm"
94 | case genre = "genreIndex"
95 | case rating = "ratingIndex"
96 | }
97 | }
98 |
99 | public struct ShortFilm: MediaType {
100 | public enum Entity: String {
101 | case shortFilm
102 | case artist = "shortFilmArtist"
103 | }
104 |
105 | public enum Attribute: String {
106 | case genre = "genreIndex"
107 | case artist = "artistTerm"
108 | case shortFilm = "shortFilmTerm"
109 | case rating = "ratingIndex"
110 | case description = "descriptionTerm"
111 | }
112 | }
113 |
114 | struct TVShow: MediaType {
115 | enum Entity: String {
116 | case episode = "tvEpisode"
117 | case season = "tvSeason"
118 | }
119 |
120 | enum Attribute: String {
121 | case genre = "genreIndex"
122 | case episode = "tvEpisodeTerm"
123 | case show = "showTerm"
124 | case season = "tvSeasonTerm"
125 | case rating = "ratingIndex"
126 | case description = "descriptionTerm"
127 | }
128 | }
129 |
130 | struct Software: MediaType {
131 | enum Entity: String {
132 | case software
133 | case iPadSoftware
134 | case macSoftware
135 | }
136 |
137 | enum Attribute: String {
138 | case softwareDeveloper
139 | }
140 | }
141 |
142 | struct EBook: MediaType {
143 | enum Entity: String {
144 | case ebook
145 | }
146 |
147 | // FIXME: Speculative
148 | enum Attribute: String {
149 | case title = "titleTerm"
150 | case author = "authorTerm"
151 | case genre = "genreIndex"
152 | case rating = "ratingIndex"
153 | }
154 | }
155 |
156 | struct All: MediaType {
157 | enum Entity: String {
158 | case movie
159 | case album
160 | case artist = "allArtist"
161 | case podcast
162 | case musicVideo
163 | case mix
164 | case audiobook
165 | case tvSeason
166 | case track = "allTrack"
167 | }
168 |
169 | enum Attribute: String {
170 | case actor = "actorTerm"
171 | case language = "languageTerm"
172 | case allArtist = "allArtistTerm"
173 | case episode = "tvEpisodeTerm"
174 | case shortFilm = "shortFilmTerm"
175 | case director = "directorTerm"
176 | case releaseYear = "releaseYearTerm"
177 | case title = "titleTerm"
178 | case featureFilm = "featureFilmTerm"
179 | case ratingIndex = "ratingIndex"
180 | case keywords = "keywordsTerm"
181 | case description = "descriptionTerm"
182 | case author = "authorTerm"
183 | case genre = "genreIndex"
184 | case mix = "mixTerm"
185 | case track = "allTrackTerm"
186 | case artist = "artistTerm"
187 | case composer = "composerTerm"
188 | case season = "tvSeasonTerm"
189 | case producer = "producerTerm"
190 | case ratingTerm = "ratingTerm"
191 | case song = "songTerm"
192 | case movieArtist = "movieArtistTerm"
193 | case show = "showTerm"
194 | case movie = "movieTerm"
195 | case album = "albumTerm"
196 | }
197 | }
198 |
199 | public struct AppleiTunesSearchURLComponents {
200 | private let scheme = "https"
201 | private let host = "itunes.apple.com"
202 | private let path = "/search"
203 |
204 | /**
205 | The URL-encoded text string you want to search for. For example: jack+johnson.
206 | */
207 | public var term: String
208 |
209 | /**
210 | The search uses the default store front for the specified country. For example: US. The default is US.
211 |
212 | The language, English or Japanese, you want to use when returning search results. Specify the language using the five-letter codename. For example: en_us.The default is en_us (English).
213 | */
214 | public var locale: Locale {
215 | willSet {
216 | precondition(newValue.regionCode != nil, "locale must have region code")
217 | }
218 | }
219 |
220 | /**
221 | The type of results you want returned, relative to the specified media type. For example: movieArtist for a movie media type search. The default is the track entity associated with the specified media type.
222 | */
223 | public var entity: Media.Entity?
224 |
225 | /**
226 | The attribute you want to search for in the stores, relative to the specified media type. For example, if you want to search for an artist by name specify entity=allArtist&attribute=allArtistTerm. In this example, if you search for term=maroon, iTunes returns “Maroon 5” in the search results, instead of all artists who have ever recorded a song with the word “maroon” in the title.
227 | The default is all attributes associated with the specified media type.
228 | */
229 | public var attribute: Media.Attribute?
230 |
231 | /**
232 | The name of the Javascript callback function you want to use when returning search results to your website. For example: wsSearchCB.
233 | */
234 | public var callback: String?
235 |
236 | /**
237 | The number of search results you want the iTunes Store to return. For example: 25.The default is 50.
238 | */
239 | public var limit: Int? {
240 | willSet {
241 | precondition((1...200).contains(newValue ?? 50), "limit must be between 1 and 200")
242 | }
243 | }
244 |
245 | /**
246 | The search result key version you want to receive back from your search.The default is 2.
247 | */
248 | public var version: Version?
249 |
250 | public enum Version: Int, Codable {
251 | case v1 = 1
252 | case v2 = 2
253 | }
254 |
255 | /**
256 | A flag indicating whether or not you want to include explicit content in your search results.The default is Yes.
257 | */
258 | public var explicit: Bool?
259 |
260 | // MARK: -
261 |
262 | private func encode(_ value: T?) -> String? where T.RawValue == String {
263 | return value?.rawValue
264 | }
265 |
266 | private func encode(_ value: T?) -> String? where T.RawValue == Int {
267 | return encode(value?.rawValue)
268 | }
269 |
270 | private func encode(_ value: Locale?) -> (country: String, language: String?)? {
271 | guard let locale = value, let country = locale.regionCode else {
272 | return nil
273 | }
274 |
275 | if let language = locale.languageCode {
276 | return (country, "\(language)_\(country)".lowercased())
277 | } else {
278 | return (country, nil)
279 | }
280 | }
281 |
282 | private func encode(_ value: Int?) -> String? {
283 | if let value = value {
284 | return "\(value)"
285 | } else {
286 | return nil
287 | }
288 | }
289 |
290 | private func encode(_ value: Bool?) -> String? {
291 | switch value {
292 | case true?:
293 | return "Yes"
294 | case false?:
295 | return "No"
296 | default:
297 | return nil
298 | }
299 | }
300 |
301 | // MARK: -
302 |
303 | public init(term: String,
304 | locale: Locale = Locale.current,
305 | entity: Media.Entity? = nil,
306 | attribute: Media.Attribute? = nil,
307 | callback: String? = nil,
308 | limit: Int? = nil,
309 | version: Version? = nil,
310 | explicit: Bool? = nil)
311 | {
312 | self.term = term
313 | self.locale = locale
314 | self.entity = entity
315 | self.attribute = attribute
316 | self.callback = callback
317 | self.limit = limit
318 | self.version = version
319 | self.explicit = explicit
320 | }
321 |
322 | // MARK: -
323 |
324 | public var url: URL? {
325 | return self.components.url
326 | }
327 |
328 | fileprivate var components: URLComponents {
329 | var components = URLComponents()
330 | components.scheme = self.scheme
331 | components.host = self.host
332 | components.path = self.path
333 | components.queryItems = self.queryItems
334 |
335 | return components
336 | }
337 |
338 | fileprivate var queryItems: [URLQueryItem] {
339 | var queryItems: [URLQueryItem] = []
340 |
341 | let termQueryItem = URLQueryItem(name: "term", value: self.term)
342 | queryItems.append(termQueryItem)
343 |
344 | if case let (country, _)? = encode(self.locale) {
345 | let queryItem = URLQueryItem(name: "country", value: country)
346 | queryItems.append(queryItem)
347 | }
348 |
349 | if let entity = encode(self.entity) {
350 | let queryItem = URLQueryItem(name: "entity", value: entity)
351 | queryItems.append(queryItem)
352 | }
353 |
354 | if let attribute = encode(self.attribute) {
355 | let queryItem = URLQueryItem(name: "attribute", value: attribute)
356 | queryItems.append(queryItem)
357 | }
358 |
359 | if let callback = self.callback {
360 | let queryItem = URLQueryItem(name: "callback", value: callback)
361 | queryItems.append(queryItem)
362 | }
363 |
364 | if let limit = encode(self.limit) {
365 | let queryItem = URLQueryItem(name: "limit", value: limit)
366 | queryItems.append(queryItem)
367 | }
368 |
369 | if case let (_, language)? = encode(self.locale) {
370 | let queryItem = URLQueryItem(name: "lang", value: language)
371 | queryItems.append(queryItem)
372 | }
373 |
374 | if let version = encode(self.version) {
375 | let queryItem = URLQueryItem(name: "version", value: version)
376 | queryItems.append(queryItem)
377 | }
378 |
379 | if let explicit = encode(self.explicit) {
380 | let queryItem = URLQueryItem(name: "explicit", value: explicit)
381 | queryItems.append(queryItem)
382 | }
383 |
384 | return queryItems
385 | }
386 | }
387 |
388 | // You can also create a lookup request to search for content in the stores based on iTunes IDs, UPCs/ EANs, and All Music Guide (AMG) IDs. ID-based lookups are faster and contain fewer false-positive results.
389 | // TODO
390 | //struct AppleiTunesLookupURLComponents {
391 | // init(id: Int, entity: MediaType.Entity? = nil, limit: Int? = nil) {
392 | //
393 | // }
394 | //}
395 |
396 | extension AppleiTunesSearchURLComponents.Version: ExpressibleByIntegerLiteral {
397 | public init(integerLiteral value: Int) {
398 | self.init(rawValue: value)!
399 | }
400 | }
401 |
402 | extension AppleiTunesSearchURLComponents: CustomStringConvertible {
403 | public var description: String {
404 | return url?.absoluteString ?? ""
405 | }
406 | }
407 |
--------------------------------------------------------------------------------
/Chapter 4/Music Store.playground/Sources/Explicitness.swift:
--------------------------------------------------------------------------------
1 | /// The Recording Industry Association of America (RIAA) parental advisory
2 | /// for the content.
3 | /// For more information,
4 | /// see http://itunes.apple.com/WebObjects/MZStore.woa/wa/parentalAdvisory
5 | public enum Explicitness: String, Decodable {
6 | /// Explicit lyrics, possibly explicit album cover
7 | case explicit
8 |
9 | /// Cleaned version with explicit lyrics "bleeped out"
10 | case cleaned
11 |
12 | /// No explicit lyrics
13 | case notExplicit
14 | }
15 |
--------------------------------------------------------------------------------
/Chapter 4/Music Store.playground/Sources/MasterViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public class MasterViewController: UITableViewController {
4 | var results: [SearchResult] = []
5 | var dataTask: URLSessionDataTask?
6 |
7 | lazy var activityIndicatorView = UIActivityIndicatorView(style: .medium)
8 |
9 | public func search(for type: T.Type, with term: String) where T: MediaType {
10 | let components = AppleiTunesSearchURLComponents(term: term)
11 | guard let url = components.url else {
12 | fatalError("Error creating URL")
13 | }
14 |
15 | self.dataTask?.cancel()
16 | self.title = term
17 |
18 | self.dataTask = URLSession.shared.dataTask(with: url) { (data, response, error) in
19 | self.activityIndicatorView.stopAnimating()
20 |
21 | guard let data = data else {
22 | fatalError()
23 | }
24 |
25 | let decoder = JSONDecoder()
26 | let searchResponse = try! decoder.decode(SearchResponse.self, from: data)
27 |
28 | self.results = searchResponse.results
29 |
30 | DispatchQueue.main.async {
31 | self.tableView.reloadData()
32 | }
33 | }
34 |
35 | self.activityIndicatorView.startAnimating()
36 | self.dataTask?.resume()
37 | }
38 |
39 | public override func viewDidLoad() {
40 | super.viewDidLoad()
41 |
42 | self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
43 |
44 | self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: self.activityIndicatorView)
45 | }
46 |
47 | public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
48 | return self.results.count
49 | }
50 |
51 | public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
52 | let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
53 |
54 | let result = self.results[indexPath.row]
55 | cell.textLabel!.text = result.trackName
56 | cell.detailTextLabel!.text = result.collectionName
57 |
58 | return cell
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Chapter 4/Music Store.playground/Sources/SearchResponse.swift:
--------------------------------------------------------------------------------
1 | public struct SearchResponse: Decodable {
2 | public let results: [SearchResult]
3 |
4 | public var nonExplicitResults: [SearchResult] {
5 | return self.results.filter { (result) in
6 | result.trackExplicitness != .explicit
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Chapter 4/Music Store.playground/Sources/SearchResult.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct SearchResult: Decodable {
4 | /// The name of the track, song, video, TV episode, and so on.
5 | public let trackName: String?
6 |
7 | /// The explicitness of the track.
8 | public let trackExplicitness: Explicitness?
9 |
10 | /// An iTunes Store URL for the content.
11 | public let trackViewURL: URL?
12 |
13 | /// A URL referencing the 30-second preview file for the content associated with the returned media type.
14 | /// - Note: This is available when media type is track.
15 | public let previewURL: URL?
16 |
17 | /// The name of the artist, and so on.
18 | public let artistName: String?
19 |
20 | /// The name of the album, TV season, audiobook, and so on.
21 | public let collectionName: String?
22 |
23 | /// A URL for the artwork associated with the returned media type,
24 | private let artworkURL100: URL?
25 |
26 | func artworkURL(size dimension: Int = 100) -> URL? {
27 | guard dimension > 0, dimension != 100,
28 | var url = self.artworkURL100 else {
29 | return self.artworkURL100
30 | }
31 |
32 | url.deleteLastPathComponent()
33 | url.appendPathComponent("\(dimension)x\(dimension)bb.jpg")
34 |
35 | return url
36 | }
37 |
38 | private enum CodingKeys: String, CodingKey {
39 | case trackName
40 | case trackExplicitness
41 | case trackViewURL = "trackViewUrl"
42 | case previewURL = "previewUrl"
43 | case artistName
44 | case collectionName
45 | case artworkURL100 = "artworkUrl100"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Chapter 4/Music Store.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter 5/In Flight Service.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import PlaygroundSupport
3 | import XCPlayground
4 |
5 | guard let url = Bundle.main.url(forResource: "Inventory", withExtension: ".plist") else {
6 | fatalError("Inventory.plist missing from main bundle")
7 | }
8 |
9 | let inventory: [Item]
10 | do {
11 | let data = try Data(contentsOf: url)
12 |
13 | let decoder = PropertyListDecoder()
14 | let plist = try decoder.decode([String: [Item]].self, from: data)
15 | inventory = plist["items"]!
16 | } catch {
17 | fatalError("Cannot load inventory \(error)")
18 | }
19 |
20 |
21 | var orders: [Order]
22 | let decoder = PropertyListDecoder()
23 | if let data = UserDefaults.standard.value(forKey: "orders") as? Data,
24 | let savedOrders = try? decoder.decode([Order].self, from: data) {
25 | orders = savedOrders
26 | } else {
27 | orders = []
28 | }
29 |
30 |
31 | let viewController = OrderSelectionViewController()
32 | let navigationController = UINavigationController(rootViewController: viewController)
33 |
34 | PlaygroundPage.current.needsIndefiniteExecution = true
35 | PlaygroundPage.current.liveView = navigationController
36 |
--------------------------------------------------------------------------------
/Chapter 5/In Flight Service.playground/Resources/Inventory.plist:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | items
7 |
8 |
9 | name
10 | Peanuts
11 | unitPrice
12 | 200
13 |
14 |
15 | name
16 | Chips
17 | unitPrice
18 | 300
19 |
20 |
21 | name
22 | Popcorn
23 | unitPrice
24 | 400
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Chapter 5/In Flight Service.playground/Sources/Item.swift:
--------------------------------------------------------------------------------
1 | public struct Item: Codable, Hashable, Equatable {
2 | public var name: String
3 | public var unitPrice: Int
4 |
5 | public static let peanuts = Item(name: "Peanuts", unitPrice: 200)
6 | public static let crackers = Item(name: "Crackers", unitPrice: 300)
7 | public static let popcorn = Item(name: "Popcorn", unitPrice: 400)
8 | }
9 |
--------------------------------------------------------------------------------
/Chapter 5/In Flight Service.playground/Sources/LineItemView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @objc
4 | public class LineItemView: UIView {
5 | public var item: String? = nil {
6 | didSet {
7 | self.itemNameLabel.text = self.item
8 | self.quantity = 0
9 | }
10 | }
11 |
12 | public var quantity: Int = 0 {
13 | didSet {
14 | self.itemQuantityLabel.text = "\(self.quantity)"
15 | }
16 | }
17 |
18 | lazy var itemNameLabel: UILabel = {
19 | let label = UILabel(frame: .zero)
20 | label.text = self.item
21 | label.font = .preferredFont(forTextStyle: .title1)
22 | label.translatesAutoresizingMaskIntoConstraints = false
23 |
24 | return label
25 | }()
26 |
27 | lazy var quantityStepper: UIStepper = {
28 | let stepper = UIStepper()
29 | stepper.value = 0
30 | stepper.stepValue = 1
31 | stepper.minimumValue = 0
32 | stepper.maximumValue = 10
33 | stepper.translatesAutoresizingMaskIntoConstraints = false
34 |
35 | stepper.addTarget(self, action: #selector(updateQuantity), for: .valueChanged)
36 |
37 | return stepper
38 | }()
39 |
40 | lazy var itemQuantityLabel: UILabel = {
41 | let label = UILabel(frame: .zero)
42 | label.font = .preferredFont(forTextStyle: .callout)
43 | label.translatesAutoresizingMaskIntoConstraints = false
44 |
45 | return label
46 | }()
47 |
48 | lazy var stackView: UIStackView = {
49 | let stackView = UIStackView(frame: self.bounds)
50 | stackView.axis = .horizontal
51 | stackView.distribution = .fillEqually
52 | stackView.alignment = .fill
53 | stackView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
54 |
55 | stackView.addArrangedSubview(self.itemNameLabel)
56 | stackView.addArrangedSubview(self.quantityStepper)
57 | stackView.addArrangedSubview(self.itemQuantityLabel)
58 |
59 | return stackView
60 | }()
61 |
62 | override init(frame: CGRect) {
63 | super.init(frame: frame)
64 |
65 | self.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
66 | self.stackView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
67 |
68 | self.addSubview(self.stackView)
69 | }
70 |
71 | @IBAction
72 | func updateQuantity() {
73 | self.quantity = Int(self.quantityStepper.value)
74 | }
75 |
76 | public required init(coder: NSCoder) {
77 | fatalError("Not Implemented")
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Chapter 5/In Flight Service.playground/Sources/Order.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct Order: Codable {
4 | private(set) var seat: String
5 |
6 | public struct LineItem: Codable {
7 | var item: Item
8 | var count: Int
9 |
10 | var price: Int {
11 | return item.unitPrice * count
12 | }
13 | }
14 | private(set) var lineItems: [LineItem]
15 |
16 | public let creationDate: Date = Date()
17 |
18 | public var totalPrice: Int {
19 | return lineItems.map{ $0.price }.reduce(0, +)
20 |
21 | // long-form
22 | // var totalPrice = 0
23 | // for lineItem in lineItems {
24 | // totalPrice += lineItem.price
25 | // }
26 | // return totalPrice
27 | }
28 |
29 | public init(seat: String, itemCounts: [Item: Int]) {
30 | self.seat = seat
31 | self.lineItems = itemCounts.compactMap{ $1 > 0 ? LineItem(item: $0, count: $1) : nil }
32 |
33 | // long-form
34 | // var lineItems: [LineItem] = []
35 | // for (item, count) in itemCounts {
36 | // let lineItem = LineItem(item: item, count: count)
37 | // lineItems.append(lineItem)
38 | // }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Chapter 5/In Flight Service.playground/Sources/OrderSelectionViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public class OrderSelectionViewController : UIViewController {
4 | lazy var pickerView: UIPickerView = {
5 | let pickerView = UIPickerView()
6 | pickerView.delegate = self
7 | pickerView.dataSource = self
8 |
9 | return pickerView
10 | }()
11 |
12 | lazy var peanutsLineItemView = LineItemView()
13 | lazy var crackersLineItemView = LineItemView()
14 | lazy var popcornLineItemView = LineItemView()
15 |
16 | lazy var saveButton: UIButton = {
17 | let button = UIButton(type: .roundedRect)
18 | button.setTitle("Save Purchase", for: .normal)
19 | button.addTarget(self, action: #selector(save), for: .touchUpInside)
20 | return button
21 | }()
22 |
23 | lazy var stackView: UIStackView = {
24 | let stackView = UIStackView()
25 | stackView.axis = .vertical
26 | stackView.distribution = .fillProportionally
27 | stackView.alignment = .fill
28 | stackView.spacing = 10
29 | stackView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
30 |
31 | stackView.addArrangedSubview(self.pickerView)
32 | stackView.addArrangedSubview(self.peanutsLineItemView)
33 | stackView.addArrangedSubview(self.crackersLineItemView)
34 | stackView.addArrangedSubview(self.popcornLineItemView)
35 | stackView.addArrangedSubview(self.saveButton)
36 |
37 | return stackView
38 | }()
39 |
40 | public override func loadView() {
41 | let view = UIView()
42 | view.backgroundColor = .white
43 | self.view = view
44 |
45 | self.view.addSubview(self.stackView)
46 | }
47 |
48 | public override func viewDidLoad() {
49 | super.viewDidLoad()
50 |
51 | self.title = "In-Flight Snacks"
52 |
53 | self.peanutsLineItemView.item = "Peanuts"
54 | self.crackersLineItemView.item = "Crackers"
55 | self.popcornLineItemView.item = "Popcorn"
56 | }
57 |
58 | @IBAction
59 | func save() {
60 | var orders: [Order]
61 | let decoder = PropertyListDecoder()
62 | if let data = UserDefaults.standard.value(forKey: "orders") as? Data,
63 | let savedOrders = try? decoder.decode([Order].self, from: data) {
64 | orders = savedOrders
65 | } else {
66 | orders = []
67 | }
68 |
69 | let order = Order(seat: self.selectedSeat, itemCounts: [
70 | .peanuts: self.peanutsLineItemView.quantity,
71 | .crackers: self.crackersLineItemView.quantity,
72 | .popcorn: self.popcornLineItemView.quantity
73 | ])
74 |
75 | orders.append(order)
76 |
77 | let encoder = PropertyListEncoder()
78 |
79 | UserDefaults.standard.set(try? encoder.encode(orders), forKey: "orders")
80 |
81 | self.crackersLineItemView.quantity = 0
82 | self.peanutsLineItemView.quantity = 0
83 | }
84 | }
85 |
86 | extension OrderSelectionViewController {
87 | private enum PickerViewComponent: Int {
88 | case label
89 | case number
90 | case letter
91 | }
92 |
93 | var numbers: [Int] {
94 | return [6, 7, 8, 9, 10]
95 | }
96 |
97 | var letters: [String] {
98 | return ["A", "B", "C", "D", "E", "F"]
99 | }
100 |
101 | var selectedSeat: String {
102 | let selectedNumber = numbers[self.pickerView.selectedRow(inComponent: PickerViewComponent.number.rawValue)]
103 | let selectedLetter = letters[self.pickerView.selectedRow(inComponent: PickerViewComponent.letter.rawValue)]
104 | return "\(selectedNumber)\(selectedLetter)"
105 | }
106 | }
107 |
108 | extension OrderSelectionViewController: UIPickerViewDataSource {
109 | public func numberOfComponents(in pickerView: UIPickerView) -> Int {
110 | return 3
111 | }
112 |
113 | public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
114 | switch PickerViewComponent(rawValue: component)! {
115 | case .label:
116 | return 1
117 | case .number:
118 | return numbers.count
119 | case .letter:
120 | return letters.count
121 | }
122 | }
123 | }
124 |
125 | extension OrderSelectionViewController: UIPickerViewDelegate {
126 | public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
127 | switch PickerViewComponent(rawValue: component)! {
128 | case .label:
129 | return "Seat"
130 | case .number:
131 | return "\(numbers[row])"
132 | case .letter:
133 | return letters[row]
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Chapter 5/In Flight Service.playground/Sources/OrdersViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public class OrdersViewController: UITableViewController {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/Chapter 5/In Flight Service.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter 6/Luggage Scanner.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import CoreData
3 | import CoreImage
4 | import PlaygroundSupport
5 |
6 | let context = mainContext()
7 |
8 | try! Passenger.insertSampleRecords(into: context)
9 |
10 | let scanner = LuggageTagScanner()
11 |
12 | let tagsAtDeparture = [#imageLiteral(resourceName: "tag.png")]
13 |
14 | // Departure
15 | do {
16 | for image in tagsAtDeparture {
17 | try scanner.scan(image: image, at: .origin, in: context)
18 | }
19 |
20 | try context.save()
21 | } catch {
22 | fatalError("\(error)")
23 | }
24 |
25 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
26 | do {
27 | for image in tagsAtDeparture {
28 | try scanner.scan(image: image, at: .destination, in: context)
29 | }
30 |
31 | try context.save()
32 | } catch {
33 | fatalError("\(error)")
34 | }
35 | }
36 |
37 | let viewController = ViewController()
38 | let navigationController = UINavigationController(rootViewController: viewController)
39 | PlaygroundPage.current.liveView = navigationController
40 |
--------------------------------------------------------------------------------
/Chapter 6/Luggage Scanner.playground/Resources/BaggageCheck.mom:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Flight-School/Guide-to-Swift-Codable-Sample-Code/64d884bfc4ad54abaf15956303ad35a8ef189e13/Chapter 6/Luggage Scanner.playground/Resources/BaggageCheck.mom
--------------------------------------------------------------------------------
/Chapter 6/Luggage Scanner.playground/Resources/BaggageCheck.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | CoreNothing.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Chapter 6/Luggage Scanner.playground/Resources/BaggageCheck.xcdatamodeld/CoreNothing.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Chapter 6/Luggage Scanner.playground/Resources/tag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Flight-School/Guide-to-Swift-Codable-Sample-Code/64d884bfc4ad54abaf15956303ad35a8ef189e13/Chapter 6/Luggage Scanner.playground/Resources/tag.png
--------------------------------------------------------------------------------
/Chapter 6/Luggage Scanner.playground/Sources/CodingUserInfoKey+Context.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension CodingUserInfoKey {
4 | public static let context = CodingUserInfoKey(rawValue: "context")!
5 | }
6 |
--------------------------------------------------------------------------------
/Chapter 6/Luggage Scanner.playground/Sources/Luggage.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import CoreData
4 |
5 | @objc(Luggage)
6 | public final class Luggage: NSManagedObject {
7 | @NSManaged public var identifier: UUID
8 | @NSManaged public var weight: Float
9 |
10 | @NSManaged public var departedAt: NSDate?
11 | @NSManaged public var arrivedAt: NSDate?
12 |
13 | @NSManaged public var owner: Passenger?
14 | }
15 |
16 | extension Luggage {
17 | public static func fetch(with identifier: UUID, from context: NSManagedObjectContext) throws -> Luggage? {
18 | var luggage: Luggage? = nil
19 | context.performAndWait {
20 | let fetchRequest = NSFetchRequest(entityName: "Luggage")
21 | fetchRequest.predicate = NSPredicate(format: "identifier == %@", identifier as CVarArg)
22 | fetchRequest.fetchLimit = 1
23 | if let results = try? context.fetch(fetchRequest) {
24 | luggage = results.first
25 | }
26 | }
27 |
28 | return luggage
29 | }
30 | }
31 |
32 | extension Luggage: Decodable {
33 | private enum CodingKeys: String, CodingKey {
34 | case identifier = "id"
35 | case weight
36 | case owner
37 | }
38 |
39 | public convenience init(from decoder: Decoder) throws {
40 | guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else {
41 | fatalError("Missing context or invalid context")
42 | }
43 |
44 | guard let entity = NSEntityDescription.entity(forEntityName: "Luggage", in: context) else {
45 | fatalError("Unknown entity Luggage in context")
46 | }
47 |
48 | self.init(entity: entity, insertInto: context)
49 |
50 | let container = try decoder.container(keyedBy: CodingKeys.self)
51 | self.identifier = try container.decode(UUID.self, forKey: .identifier)
52 | self.weight = try container.decode(Float.self, forKey: .weight)
53 | self.owner = try container.decodeIfPresent(Passenger.self, forKey: .owner)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Chapter 6/Luggage Scanner.playground/Sources/LuggageTagScanner.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreData
3 | import CoreImage
4 | import UIKit
5 |
6 | public class LuggageTagScanner {
7 | private lazy var detector: CIDetector? = {
8 | let context = CIContext()
9 | let options = [CIDetectorAccuracy: CIDetectorAccuracyHigh]
10 | let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: context, options: options)
11 | return detector
12 | }()
13 |
14 | public required init() {}
15 |
16 | public enum Point {
17 | case origin, destination
18 | }
19 |
20 | public func scan(image uiImage: UIImage, at point: Point, in context: NSManagedObjectContext) throws {
21 | guard
22 | let ciImage = CIImage(image: uiImage),
23 | let features = detector?.features(in: ciImage),
24 | let qrCode = features.first as? CIQRCodeFeature,
25 | let data = qrCode.messageString?.data(using: .utf8) else {
26 | return
27 | }
28 |
29 | let decoder = JSONDecoder()
30 | decoder.userInfo[.context] = context
31 |
32 | let luggage = try decoder.decode(Luggage.self, from: data)
33 |
34 | switch point {
35 | case .origin:
36 | luggage.departedAt = NSDate()
37 | case .destination:
38 | luggage.arrivedAt = NSDate()
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Chapter 6/Luggage Scanner.playground/Sources/Passenger.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import CoreData
4 |
5 | @objc(Passenger)
6 | public final class Passenger: NSManagedObject {
7 | @NSManaged public var givenName: String
8 | @NSManaged public var familyName: String
9 |
10 | @NSManaged public var luggage: NSSet?
11 | }
12 |
13 | extension Passenger: Decodable {
14 | public convenience init(from decoder: Decoder) throws {
15 | guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else {
16 | fatalError("Missing context or invalid context")
17 | }
18 |
19 | guard let entity = NSEntityDescription.entity(forEntityName: "Passenger", in: context) else {
20 | fatalError("Unknown entity Passenger in context")
21 | }
22 |
23 | self.init(entity: entity, insertInto: context)
24 |
25 | var container = try decoder.unkeyedContainer()
26 | self.givenName = try container.decode(String.self)
27 | self.familyName = try container.decode(String.self)
28 | }
29 | }
30 |
31 | extension Passenger {
32 | public static func insertSampleRecords(into context: NSManagedObjectContext) throws {
33 | let fetchRequest = NSFetchRequest(entityName: "Passenger")
34 | guard try context.count(for: fetchRequest) == 0 else {
35 | return
36 | }
37 |
38 | let names: [[String]] = [
39 | ["J", "LEE"],
40 | ["D", "MEN"],
41 | ["L", "MEN"],
42 | ["A", "THO"],
43 | ["A", "ZMU"],
44 | ["D", "ZMU"]
45 | ]
46 |
47 | for name in names {
48 | guard let givenName = name.first, let familyName = name.last else {
49 | continue
50 | }
51 |
52 | let passenger = Passenger(entity: Passenger.entity(), insertInto: context)
53 | passenger.givenName = givenName
54 | passenger.familyName = familyName
55 | }
56 |
57 | try context.save()
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Chapter 6/Luggage Scanner.playground/Sources/Support.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreData
3 |
4 | private let container: NSPersistentContainer = {
5 | guard let url = Bundle.main.url(forResource: "BaggageCheck", withExtension: ".mom") else {
6 | fatalError("BaggageCheck.mom missing from main bundle")
7 | }
8 |
9 | guard let model = NSManagedObjectModel(contentsOf: url) else {
10 | fatalError("Invalid managed object model file")
11 | }
12 |
13 | let container = NSPersistentContainer(name: "BaggageCheck", managedObjectModel: model)
14 | container.loadPersistentStores { (_, error) in
15 | if let error = error as NSError? {
16 | fatalError("Unresolved error \(error), \(error.userInfo)")
17 | }
18 | }
19 |
20 | return container
21 | }()
22 |
23 | public func mainContext() -> NSManagedObjectContext {
24 | let context = container.viewContext
25 | context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
26 |
27 | return context
28 | }
29 |
--------------------------------------------------------------------------------
/Chapter 6/Luggage Scanner.playground/Sources/ViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import CoreData
3 |
4 | public class ViewController : UITableViewController {
5 | lazy var fetchedResultsController: NSFetchedResultsController = {
6 | let fetchRequest = NSFetchRequest(entityName: "Passenger")
7 | fetchRequest.sortDescriptors = [NSSortDescriptor(key: "familyName", ascending: true), NSSortDescriptor(key: "givenName", ascending: true)]
8 | fetchRequest.relationshipKeyPathsForPrefetching = ["Luggage"]
9 |
10 | return NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: mainContext(), sectionNameKeyPath: nil, cacheName: nil)
11 | }()
12 |
13 | public override func viewDidLoad() {
14 | super.viewDidLoad()
15 |
16 | self.title = "Passengers"
17 |
18 | do {
19 | try self.fetchedResultsController.performFetch()
20 | } catch {
21 | print(error)
22 | }
23 |
24 | self.tableView.reloadData()
25 | }
26 |
27 | public override func numberOfSections(in tableView: UITableView) -> Int {
28 | return self.fetchedResultsController.sections!.count
29 | }
30 |
31 | public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
32 | let sectionInfo = self.fetchedResultsController.sections![section]
33 | return sectionInfo.numberOfObjects
34 | }
35 |
36 | public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
37 | let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
38 | let passenger = self.fetchedResultsController.object(at: indexPath) as! Passenger
39 |
40 | cell.textLabel?.text = "\(passenger.givenName)/\(passenger.familyName)"
41 | cell.detailTextLabel?.text = "Bags: \(passenger.luggage?.count ?? 0)"
42 |
43 | return cell
44 | }
45 |
46 | public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
47 | return self.fetchedResultsController.sections![section].name
48 | }
49 |
50 | public override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
51 | return self.fetchedResultsController.sectionIndexTitles
52 | }
53 |
54 | public override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
55 | return self.fetchedResultsController.section(forSectionIndexTitle: title, at: index)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Chapter 6/Luggage Scanner.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter 7/MessagePackEncoder.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct Plane: Codable {
4 | var manufacturer: String
5 | var model: String
6 | var seats: Int
7 | }
8 |
9 | let plane = Plane(manufacturer: "Cirrus",
10 | model: "SR22",
11 | seats: 4)
12 |
13 | let encoder = MessagePackEncoder()
14 | let data = try! encoder.encode(plane)
15 |
16 | print(data.map { String(format:"%02X", $0) })
17 |
--------------------------------------------------------------------------------
/Chapter 7/MessagePackEncoder.playground/Sources/KeyedValueContainer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension _MessagePackEncoder {
4 | class KeyedContainer where Key: CodingKey {
5 | private var storage: [String: MessagePackEncodingContainer] = [:]
6 |
7 | var codingPath: [CodingKey]
8 | var userInfo: [CodingUserInfoKey: Any]
9 |
10 | func nestedCodingPath(forKey key: CodingKey) -> [CodingKey] {
11 | return self.codingPath + [key]
12 | }
13 |
14 | init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
15 | self.codingPath = codingPath
16 | self.userInfo = userInfo
17 | }
18 | }
19 | }
20 |
21 | extension _MessagePackEncoder.KeyedContainer: KeyedEncodingContainerProtocol {
22 | func encodeNil(forKey key: Key) throws {
23 | var container = self.nestedSingleValueContainer(forKey: key)
24 | try container.encodeNil()
25 | }
26 |
27 | func encode(_ value: T, forKey key: Key) throws where T : Encodable {
28 | var container = self.nestedSingleValueContainer(forKey: key)
29 | try container.encode(value)
30 | }
31 |
32 | private func nestedSingleValueContainer(forKey key: Key) -> SingleValueEncodingContainer {
33 | let container = _MessagePackEncoder.SingleValueContainer(codingPath: self.nestedCodingPath(forKey: key), userInfo: self.userInfo)
34 | self.storage[key.stringValue] = container
35 | return container
36 | }
37 |
38 | func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
39 | let container = _MessagePackEncoder.UnkeyedContainer(codingPath: self.nestedCodingPath(forKey: key), userInfo: self.userInfo)
40 | self.storage[key.stringValue] = container
41 |
42 | return container
43 | }
44 |
45 | func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey {
46 | let container = _MessagePackEncoder.KeyedContainer(codingPath: self.nestedCodingPath(forKey: key), userInfo: self.userInfo)
47 | self.storage[key.stringValue] = container
48 |
49 | return KeyedEncodingContainer(container)
50 | }
51 |
52 | func superEncoder() -> Encoder {
53 | fatalError("Unimplemented")
54 | }
55 |
56 | func superEncoder(forKey key: Key) -> Encoder {
57 | fatalError("Unimplemented")
58 | }
59 | }
60 |
61 | extension _MessagePackEncoder.KeyedContainer: MessagePackEncodingContainer {
62 | var data: Data {
63 | var data = Data()
64 |
65 | let length = storage.count
66 | if let uint16 = UInt16(exactly: length) {
67 | if length <= 15 {
68 | data.append(0x80 + UInt8(length))
69 | } else {
70 | data.append(0xdc)
71 | data.append(contentsOf: uint16.bytes)
72 | }
73 | } else if let uint32 = UInt32(exactly: length) {
74 | data.append(0xdd)
75 | data.append(contentsOf: uint32.bytes)
76 | } else {
77 | fatalError()
78 | }
79 |
80 | for (key, container) in self.storage {
81 | let keyContainer = _MessagePackEncoder.SingleValueContainer(codingPath: self.codingPath, userInfo: self.userInfo)
82 | try! keyContainer.encode(key)
83 | data.append(keyContainer.data)
84 |
85 | data.append(container.data)
86 | }
87 |
88 | return data
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Chapter 7/MessagePackEncoder.playground/Sources/MessagePackEncoder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class MessagePackEncoder {
4 | public init() {
5 |
6 | }
7 |
8 | public func encode(_ value: Encodable) throws -> Data {
9 | let encoder = _MessagePackEncoder()
10 | try value.encode(to: encoder)
11 | return encoder.data
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Chapter 7/MessagePackEncoder.playground/Sources/Numeric+Bytes.swift:
--------------------------------------------------------------------------------
1 | extension FixedWidthInteger {
2 | init(bytes: [UInt8]) {
3 | self = bytes.withUnsafeBufferPointer {
4 | $0.baseAddress!.withMemoryRebound(to: Self.self, capacity: 1) {
5 | $0.pointee
6 | }
7 | }
8 | }
9 |
10 | var bytes: [UInt8] {
11 | let capacity = MemoryLayout.size
12 | var mutableValue = self.bigEndian
13 | return withUnsafePointer(to: &mutableValue) {
14 | return $0.withMemoryRebound(to: UInt8.self, capacity: capacity) {
15 | return Array(UnsafeBufferPointer(start: $0, count: capacity))
16 | }
17 | }
18 | }
19 | }
20 |
21 | extension Float {
22 | var bytes: [UInt8] {
23 | return self.bitPattern.bytes
24 | }
25 | }
26 |
27 | extension Double {
28 | var bytes: [UInt8] {
29 | return self.bitPattern.bytes
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Chapter 7/MessagePackEncoder.playground/Sources/SingleValueContainer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension _MessagePackEncoder {
4 | class SingleValueContainer: SingleValueEncodingContainer {
5 | private var storage: Data = Data()
6 |
7 | fileprivate var canEncodeNewValue = true
8 | fileprivate func checkCanEncode(value: Any?) throws {
9 | guard self.canEncodeNewValue else {
10 | let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Attempt to encode value through single value container when previously value already encoded.")
11 | throw EncodingError.invalidValue(value as Any, context)
12 | }
13 | }
14 |
15 | var codingPath: [CodingKey]
16 | var userInfo: [CodingUserInfoKey: Any]
17 |
18 | init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
19 | self.codingPath = codingPath
20 | self.userInfo = userInfo
21 | }
22 |
23 | // MARK -
24 |
25 | func encodeNil() throws {
26 | try checkCanEncode(value: nil)
27 | defer { self.canEncodeNewValue = false }
28 |
29 | self.storage.append(0xc0)
30 | }
31 |
32 | func encode(_ value: Bool) throws {
33 | try checkCanEncode(value: nil)
34 | defer { self.canEncodeNewValue = false }
35 |
36 | switch value {
37 | case false:
38 | self.storage.append(0xc2)
39 | case true:
40 | self.storage.append(0xc3)
41 | }
42 | }
43 |
44 | func encode(_ value: String) throws {
45 | try checkCanEncode(value: nil)
46 | defer { self.canEncodeNewValue = false }
47 |
48 | guard let data = value.data(using: .utf8) else {
49 | let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode string using UTF-8 encoding.")
50 | throw EncodingError.invalidValue(value, context)
51 | }
52 |
53 | let length = data.count
54 | if let uint8 = UInt8(exactly: length) {
55 | if (uint8 <= 31) {
56 | self.storage.append(0xa0 + uint8)
57 | } else {
58 | self.storage.append(0xd9)
59 | self.storage.append(contentsOf: uint8.bytes)
60 | }
61 | } else if let uint16 = UInt16(exactly: length) {
62 | self.storage.append(0xda)
63 | self.storage.append(contentsOf: uint16.bytes)
64 | } else if let uint32 = UInt32(exactly: length) {
65 | self.storage.append(0xdb)
66 | self.storage.append(contentsOf: uint32.bytes)
67 | } else {
68 | let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode string with length \(length).")
69 | throw EncodingError.invalidValue(value, context)
70 | }
71 |
72 | self.storage.append(data)
73 | }
74 |
75 | func encode(_ value: Double) throws {
76 | try checkCanEncode(value: nil)
77 | defer { self.canEncodeNewValue = false }
78 |
79 | self.storage.append(0xcb)
80 | self.storage.append(contentsOf: value.bytes)
81 | }
82 |
83 | func encode(_ value: Float) throws {
84 | try checkCanEncode(value: nil)
85 | defer { self.canEncodeNewValue = false }
86 |
87 | self.storage.append(0xca)
88 | self.storage.append(contentsOf: value.bytes)
89 | }
90 |
91 | func encode(_ value: Int) throws {
92 | try checkCanEncode(value: nil)
93 | defer { self.canEncodeNewValue = false }
94 |
95 | if let int8 = Int8(exactly: value) {
96 | if (int8 >= 0 && int8 <= 127) {
97 | self.storage.append(UInt8(int8))
98 | } else if (int8 < 0 && int8 >= -31) {
99 | self.storage.append(0xe0 + (0x1f & UInt8(truncatingIfNeeded: int8)))
100 | } else {
101 | try encode(int8)
102 | }
103 | } else if let int16 = Int16(exactly: value) {
104 | try encode(int16)
105 | } else if let int32 = Int32(exactly: value) {
106 | try encode(int32)
107 | } else if let int64 = Int64(exactly: value) {
108 | try encode(int64)
109 | } else {
110 | let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode integer \(value).")
111 | throw EncodingError.invalidValue(value, context)
112 | }
113 | }
114 |
115 | func encode(_ value: Int8) throws {
116 | try checkCanEncode(value: nil)
117 | defer { self.canEncodeNewValue = false }
118 |
119 | self.storage.append(0xd0)
120 | self.storage.append(contentsOf: value.bytes)
121 | }
122 |
123 | func encode(_ value: Int16) throws {
124 | try checkCanEncode(value: nil)
125 | defer { self.canEncodeNewValue = false }
126 |
127 | self.storage.append(0xd1)
128 | self.storage.append(contentsOf: value.bytes)
129 | }
130 |
131 | func encode(_ value: Int32) throws {
132 | try checkCanEncode(value: nil)
133 | defer { self.canEncodeNewValue = false }
134 |
135 | self.storage.append(0xd2)
136 | self.storage.append(contentsOf: value.bytes)
137 | }
138 |
139 | func encode(_ value: Int64) throws {
140 | try checkCanEncode(value: nil)
141 | defer { self.canEncodeNewValue = false }
142 |
143 | self.storage.append(0xd3)
144 | self.storage.append(contentsOf: value.bytes)
145 | }
146 |
147 | func encode(_ value: UInt) throws {
148 | try checkCanEncode(value: nil)
149 | defer { self.canEncodeNewValue = false }
150 |
151 | if let uint8 = UInt8(exactly: value) {
152 | if (uint8 <= 127) {
153 | self.storage.append(uint8)
154 | } else {
155 | try encode(uint8)
156 | }
157 | } else if let uint16 = UInt16(exactly: value) {
158 | try encode(uint16)
159 | } else if let uint32 = UInt32(exactly: value) {
160 | try encode(uint32)
161 | } else if let uint64 = UInt64(exactly: value) {
162 | try encode(uint64)
163 | } else {
164 | let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode unsigned integer \(value).")
165 | throw EncodingError.invalidValue(value, context)
166 | }
167 | }
168 |
169 | func encode(_ value: UInt8) throws {
170 | try checkCanEncode(value: nil)
171 | defer { self.canEncodeNewValue = false }
172 |
173 | self.storage.append(0xcc)
174 | self.storage.append(contentsOf: value.bytes)
175 | }
176 |
177 | func encode(_ value: UInt16) throws {
178 | try checkCanEncode(value: nil)
179 | defer { self.canEncodeNewValue = false }
180 |
181 | self.storage.append(0xcd)
182 | self.storage.append(contentsOf: value.bytes)
183 | }
184 |
185 | func encode(_ value: UInt32) throws {
186 | try checkCanEncode(value: nil)
187 | defer { self.canEncodeNewValue = false }
188 |
189 | self.storage.append(0xce)
190 | self.storage.append(contentsOf: value.bytes)
191 | }
192 |
193 | func encode(_ value: UInt64) throws {
194 | try checkCanEncode(value: nil)
195 | defer { self.canEncodeNewValue = false }
196 |
197 | self.storage.append(0xcf)
198 | self.storage.append(contentsOf: value.bytes)
199 | }
200 |
201 | func encode(_ value: Date) throws {
202 | try checkCanEncode(value: nil)
203 | defer { self.canEncodeNewValue = false }
204 |
205 | self.storage.append(0xd6)
206 | try encode(-1 as Int)
207 | try encode(UInt32(value.timeIntervalSince1970.rounded(.down)))
208 | }
209 |
210 | func encode(_ value: Data) throws {
211 | let length = value.count
212 | if let uint8 = UInt8(exactly: length) {
213 | self.storage.append(0xc4)
214 | try encode(uint8)
215 | self.storage.append(value)
216 | } else if let uint16 = UInt16(exactly: length) {
217 | self.storage.append(0xc5)
218 | try encode(uint16)
219 | self.storage.append(value)
220 | } else if let uint32 = UInt32(exactly: length) {
221 | self.storage.append(0xc6)
222 | try encode(uint32)
223 | self.storage.append(value)
224 | } else {
225 | let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode data of length \(value.count).")
226 | throw EncodingError.invalidValue(value, context)
227 | }
228 | }
229 |
230 | func encode(_ value: T) throws where T : Encodable {
231 | try checkCanEncode(value: nil)
232 | defer { self.canEncodeNewValue = false }
233 |
234 | let encoder = _MessagePackEncoder()
235 | try value.encode(to: encoder)
236 | self.storage.append(encoder.data)
237 | }
238 | }
239 | }
240 |
241 | extension _MessagePackEncoder.SingleValueContainer: MessagePackEncodingContainer {
242 | var data: Data {
243 | return storage
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/Chapter 7/MessagePackEncoder.playground/Sources/UnkeyedValueContainer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension _MessagePackEncoder {
4 | class UnkeyedContainer {
5 | struct Index: CodingKey {
6 | var intValue: Int?
7 |
8 | var stringValue: String {
9 | return "\(self.intValue!)"
10 | }
11 |
12 | init?(intValue: Int) {
13 | self.intValue = intValue
14 | }
15 |
16 | init?(stringValue: String) {
17 | return nil
18 | }
19 | }
20 |
21 | private var storage: [MessagePackEncodingContainer] = []
22 |
23 | var count: Int {
24 | return storage.count
25 | }
26 |
27 | var codingPath: [CodingKey]
28 |
29 | var nestedCodingPath: [CodingKey] {
30 | return self.codingPath + [Index(intValue: self.count)!]
31 | }
32 |
33 | var userInfo: [CodingUserInfoKey: Any]
34 |
35 | init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
36 | self.codingPath = codingPath
37 | self.userInfo = userInfo
38 | }
39 | }
40 | }
41 |
42 | extension _MessagePackEncoder.UnkeyedContainer: UnkeyedEncodingContainer {
43 | func encodeNil() throws {
44 | var container = self.nestedSingleValueContainer()
45 | try container.encodeNil()
46 | }
47 |
48 | func encode(_ value: T) throws where T : Encodable {
49 | var container = self.nestedSingleValueContainer()
50 | try container.encode(value)
51 | }
52 |
53 | private func nestedSingleValueContainer() -> SingleValueEncodingContainer {
54 | return _MessagePackEncoder.SingleValueContainer(codingPath: self.nestedCodingPath, userInfo: self.userInfo)
55 | }
56 |
57 | func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey {
58 | let container = _MessagePackEncoder.KeyedContainer(codingPath: self.nestedCodingPath, userInfo: self.userInfo)
59 | self.storage.append(container)
60 |
61 | return KeyedEncodingContainer(container)
62 | }
63 |
64 | func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
65 | let container = _MessagePackEncoder.UnkeyedContainer(codingPath: self.nestedCodingPath, userInfo: self.userInfo)
66 | self.storage.append(container)
67 |
68 | return container
69 | }
70 |
71 | func superEncoder() -> Encoder {
72 | fatalError("Unimplemented")
73 | }
74 | }
75 |
76 | extension _MessagePackEncoder.UnkeyedContainer: MessagePackEncodingContainer {
77 | var data: Data {
78 | var data = Data()
79 |
80 | let length = storage.count
81 | if let uint16 = UInt16(exactly: length) {
82 | if uint16 <= 15 {
83 | data.append(UInt8(0x90 + uint16))
84 | } else {
85 | data.append(0xdc)
86 | data.append(contentsOf: uint16.bytes)
87 | }
88 | } else if let uint32 = UInt32(exactly: length) {
89 | data.append(0xdc)
90 | data.append(contentsOf: uint32.bytes)
91 | } else {
92 | fatalError()
93 | }
94 |
95 | for container in storage {
96 | data.append(container.data)
97 | }
98 |
99 | return data
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Chapter 7/MessagePackEncoder.playground/Sources/_MessagePackEncoder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | protocol MessagePackEncodingContainer {
4 | var data: Data { get }
5 | }
6 |
7 | class _MessagePackEncoder {
8 | var codingPath: [CodingKey] = []
9 |
10 | var userInfo: [CodingUserInfoKey : Any] = [:]
11 |
12 | fileprivate var container: MessagePackEncodingContainer?
13 |
14 | var data: Data {
15 | return container?.data ?? Data()
16 | }
17 | }
18 |
19 | extension _MessagePackEncoder: Encoder {
20 | fileprivate func assertCanCreateContainer() {
21 | precondition(self.container == nil)
22 | }
23 |
24 | func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey {
25 | assertCanCreateContainer()
26 |
27 | let container = KeyedContainer(codingPath: self.codingPath, userInfo: self.userInfo)
28 | self.container = container
29 |
30 | return KeyedEncodingContainer(container)
31 | }
32 |
33 | func unkeyedContainer() -> UnkeyedEncodingContainer {
34 | assertCanCreateContainer()
35 |
36 | let container = UnkeyedContainer(codingPath: self.codingPath, userInfo: self.userInfo)
37 | self.container = container
38 |
39 | return container
40 | }
41 |
42 | func singleValueContainer() -> SingleValueEncodingContainer {
43 | assertCanCreateContainer()
44 |
45 | let container = SingleValueContainer(codingPath: self.codingPath, userInfo: self.userInfo)
46 | self.container = container
47 |
48 | return container
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Chapter 7/MessagePackEncoder.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2018 Read Evaluate Press, LLC
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a
4 | copy of this software and associated documentation files (the "Software"),
5 | to deal in the Software without restriction, including without limitation
6 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | and/or sell copies of the Software, and to permit persons to whom the
8 | Software is furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
14 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Guide to Swift Codable Sample Code
6 |
7 | [![Build Status][build status badge]][build status]
8 | [![License][license badge]][license]
9 | [![Swift Version][swift version badge]][swift version]
10 |
11 | This repository contains sample code used in the
12 | [Flight School Guide to Swift Codable](https://flight.school/books/codable).
13 |
14 | ---
15 |
16 | ### Chapter 1
17 |
18 | Chapter 1 introduces `Codable` by way of a round-trip journey ---
19 | going from model to JSON representation and back again.
20 |
21 | #### Plane
22 |
23 | ```swift
24 | let json = """
25 | {
26 | "manufacturer": "Cessna",
27 | "model": "172 Skyhawk",
28 | "seats": 4,
29 | }
30 | """.data(using: .utf8)!
31 |
32 | let decoder = JSONDecoder()
33 | let plane = try! decoder.decode(Plane.self, from: json)
34 | ```
35 |
36 | ### Chapter 2
37 |
38 | Chapter 2 follows a more complicated example with
39 | nested structures, mismatched keys, and timestamps.
40 |
41 | #### Flight Plan
42 |
43 | ```swift
44 | let json = """
45 | {
46 | "aircraft": {
47 | "identification": "NA12345",
48 | "color": "Blue/White"
49 | },
50 | "route": ["KTTD", "KHIO"],
51 | "departure_time": {
52 | "proposed": "2018-04-20T15:07:24-07:00",
53 | "actual": "2018-04-20T15:07:24-07:00"
54 | },
55 | "flight_rules": "IFR",
56 | "remarks": null
57 | }
58 | """.data(using: .utf8)!
59 |
60 | let decoder = JSONDecoder()
61 | decoder.dateDecodingStrategy = .iso8601
62 |
63 | let plan = try! decoder.decode(FlightPlan.self, from: json)
64 | ```
65 |
66 | ### Chapter 3
67 |
68 | Chapter 3 shows what to do when `Codable` conformance
69 | can’t be synthesized by the compiler.
70 |
71 | In the process, we share an implementation of a type-erased
72 | [`AnyCodable`](https://github.com/Flight-School/AnyCodable) type.
73 |
74 | #### AnyDecodable
75 |
76 | ```swift
77 | struct Report: Decodable {
78 | var title: String
79 | var body: String
80 | var metadata: [String: AnyDecodable]
81 | }
82 | ```
83 |
84 | #### Coordinates
85 |
86 | ```swift
87 | let json = """
88 | {
89 | "coordinates": [
90 | {
91 | "latitude": 37.332,
92 | "longitude": -122.011
93 | },
94 | [-122.011, 37.332],
95 | "37.332, -122.011"
96 | ]
97 | }
98 | """.data(using: .utf8)!
99 |
100 | let decoder = JSONDecoder()
101 | let coordinates = try! decoder.decode([String: [Coordinate]].self, from: json)["coordinates"]
102 | ```
103 |
104 | #### EconomySeat
105 |
106 | ```swift
107 | class EconomySeat: Decodable {
108 | var number: Int
109 | var letter: String
110 | // ...
111 | }
112 |
113 | class PremiumEconomySeat: EconomySeat {
114 | var mealPreference: String?
115 | // ...
116 | }
117 |
118 | let json = """
119 | {
120 | "number": 7,
121 | "letter": "A",
122 | "mealPreference": "vegetarian"
123 | }
124 | """.data(using: .utf8)!
125 |
126 | let decoder = JSONDecoder()
127 | let seat = try! decoder.decode(PremiumEconomySeat.self, from: json)
128 | ```
129 |
130 | #### EitherBirdOrPlane
131 |
132 | ```swift
133 | let json = """
134 | [
135 | {
136 | "type": "bird",
137 | "genus": "Chaetura",
138 | "species": "Vauxi"
139 | },
140 | {
141 | "type": "plane",
142 | "identifier": "NA12345"
143 | }
144 | ]
145 | """.data(using: .utf8)!
146 |
147 | let decoder = JSONDecoder()
148 | let objects = try! decoder.decode([Either].self, from: json)
149 | ```
150 |
151 | #### FuelPrice
152 |
153 | ```swift
154 | protocol FuelPrice {
155 | var type: Fuel { get }
156 | var pricePerLiter: Double { get }
157 | var currency: String { get }
158 | }
159 |
160 | struct CanadianFuelPrice: Decodable {
161 | let type: Fuel
162 | let price: Double /// CAD / liter
163 | }
164 |
165 | extension CanadianFuelPrice: FuelPrice {
166 | var pricePerLiter: Double {
167 | return self.price
168 | }
169 |
170 | var currency: String {
171 | return "CAD"
172 | }
173 | }
174 | ```
175 |
176 | #### Pixel
177 |
178 | ```swift
179 | let encoder = JSONEncoder()
180 | encoder.userInfo[.colorEncodingStrategy] =
181 | ColorEncodingStrategy.hexadecimal(hash: true)
182 |
183 | let cyan = Pixel(red: 0, green: 255, blue: 255)
184 | let magenta = Pixel(red: 255, green: 0, blue: 255)
185 | let yellow = Pixel(red: 255, green: 255, blue: 0)
186 | let black = Pixel(red: 0, green: 0, blue: 0)
187 |
188 | let json = try! encoder.encode([cyan, magenta, yellow, black])
189 | ```
190 |
191 | #### Route
192 |
193 | ```swift
194 | let json = """
195 | {
196 | "points": ["KSQL", "KWVI"],
197 | "KSQL": {
198 | "code": "KSQL",
199 | "name": "San Carlos Airport"
200 | },
201 | "KWVI": {
202 | "code": "KWVI",
203 | "name": "Watsonville Municipal Airport"
204 | }
205 | }
206 | """.data(using: .utf8)!
207 |
208 | let decoder = JSONDecoder()
209 | let route = try decoder.decode(Route.self, from: json)
210 | ```
211 |
212 | ### Chapter 4
213 |
214 | Chapter 4 is a case study in which you build search functionality
215 | for a music store app using the iTunes Search API
216 | (but really, it’s a lesson about command-line tools and epistemology).
217 |
218 | We also released
219 | [`AppleiTunesSearchURLComponents`](https://github.com/Flight-School/AppleiTunesSearchURLComponents)
220 | as a standalone component.
221 |
222 | #### Music Store
223 |
224 | ```swift
225 | viewController.search(for: Music.self, with: <#artist#>)
226 | ```
227 |
228 | ### Chapter 5
229 |
230 | Chapter 5 shows you how to use `Codable` with `UserDefaults`
231 | by way of an example app for tabulating in-flight snack orders.
232 |
233 | #### In Flight Service
234 |
235 | ```swift
236 | guard let url = Bundle.main.url(forResource: "Inventory", withExtension: ".plist") else {
237 | fatalError("Inventory.plist missing from main bundle")
238 | }
239 |
240 | let inventory: [Item]
241 | do {
242 | let data = try Data(contentsOf: url)
243 |
244 | let decoder = PropertyListDecoder()
245 | let plist = try decoder.decode([String: [Item]].self, from: data)
246 | inventory = plist["items"]!
247 | } catch {
248 | fatalError("Cannot load inventory \(error)")
249 | }
250 | ```
251 |
252 | ### Chapter 6
253 |
254 | Chapter 6 is about how Codable fits into a Core Data stack.
255 | The example app for this chapter is a luggage tag scanner
256 | that reads JSON from QR codes.
257 |
258 | #### Luggage Scanner
259 |
260 | ```swift
261 | do {
262 | for image in tagsAtDeparture {
263 | try scanner.scan(image: image, at: .origin, in: context)
264 | }
265 |
266 | try context.save()
267 | } catch {
268 | fatalError("\(error)")
269 | }
270 | ```
271 |
272 | ### Chapter 7
273 |
274 | Chapter 7 is a doozy. It walks through a complete implementation of a Codable-compatible encoder for the MessagePack format, from start to finish.
275 |
276 | A complete `Codable`-compliant implementation is available at
277 | [Flight-School/MessagePack](https://github.com/Flight-School/MessagePack).
278 | If you're interested in building your own `Codable` encoder or decoder,
279 | check out our [DIY Kit](https://github.com/Flight-School/Codable-DIY-Kit).
280 |
281 | #### MessagePackEncoder
282 |
283 | ```swift
284 | let plane = Plane(manufacturer: "Cirrus",
285 | model: "SR22",
286 | seats: 4)
287 |
288 | let encoder = MessagePackEncoder()
289 | let data = try! encoder.encode(plane)
290 | ```
291 |
292 | ## License
293 |
294 | MIT
295 |
296 | ## About Flight School
297 |
298 | Flight School is a book series for advanced Swift developers
299 | that explores essential topics in iOS and macOS development
300 | through concise, focused guides.
301 |
302 | If you'd like to get in touch,
303 | feel free to [message us on Twitter](https://twitter.com/flightdotschool)
304 | or email us at .
305 |
306 | [build status]: https://travis-ci.org/Flight-School/Guide-to-Swift-Codable-Sample-Code
307 | [build status badge]: https://api.travis-ci.com/Flight-School/Guide-to-Swift-Codable-Sample-Code.svg?branch=master
308 | [license]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat
309 | [license badge]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat
310 | [swift version]: https://swift.org/download/
311 | [swift version badge]: http://img.shields.io/badge/swift%20version-5.0-orange.svg?style=flat
312 |
--------------------------------------------------------------------------------
/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Flight-School/Guide-to-Swift-Codable-Sample-Code/64d884bfc4ad54abaf15956303ad35a8ef189e13/cover.jpg
--------------------------------------------------------------------------------