├── .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 | Flight School Guide to Swift Codable Cover 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 --------------------------------------------------------------------------------