From 8b3b947406f5c3a53a16fff19064d4d6b0bb636d Mon Sep 17 00:00:00 2001 From: John Estropia Date: Thu, 20 Aug 2020 00:39:03 +0900 Subject: [PATCH] pokedex demo --- Demo/Demo.xcodeproj/project.pbxproj | 12 +- .../Modern.PlacemarksDemo.Place.swift | 17 +- .../Modern.PokedexDemo.ItemView.swift | 65 +++++- .../Modern.PokedexDemo.MainView.swift | 1 - .../Modern.PokedexDemo.PokedexEntry.swift | 18 +- .../Modern.PokedexDemo.PokemonDisplay.swift | 83 +++++++ .../Modern.PokedexDemo.PokemonForm.swift | 110 ++++++++- .../Modern.PokedexDemo.PokemonSpecies.swift | 30 --- .../Modern.PokedexDemo.PokemonType.swift | 26 +++ .../Modern.PokedexDemo.Service.swift | 212 ++++++++++++++---- .../PokedexDemo/Modern.PokedexDemo.swift | 2 +- Demo/Sources/Helpers/NetworkImageView.swift | 107 +++++++++ 12 files changed, 575 insertions(+), 108 deletions(-) create mode 100644 Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonDisplay.swift delete mode 100644 Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonSpecies.swift create mode 100644 Demo/Sources/Helpers/NetworkImageView.swift diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 265640e..a2dbb99 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ B531EFEB24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = B531EFEA24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift */; }; B531EFED24EB7453005F247D /* Modern.PokedexDemo.MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B531EFEC24EB7453005F247D /* Modern.PokedexDemo.MainView.swift */; }; B54269C624ED190C00A66C23 /* Modern.PokedexDemo.ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54269C524ED190C00A66C23 /* Modern.PokedexDemo.ItemView.swift */; }; + B566C8E624ED6B98001134A1 /* NetworkImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B566C8E524ED6B98001134A1 /* NetworkImageView.swift */; }; B5A3911D24E5429200E7E8BD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3911C24E5429200E7E8BD /* AppDelegate.swift */; }; B5A3911F24E5429200E7E8BD /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */; }; B5A3912124E5429200E7E8BD /* Menu.MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3912024E5429200E7E8BD /* Menu.MainView.swift */; }; @@ -61,7 +62,7 @@ B5A391AC24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391AB24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift */; }; B5A391AE24E9150F00E7E8BD /* Modern.ColorsDemo.UIKit.DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391AD24E9150F00E7E8BD /* Modern.ColorsDemo.UIKit.DetailViewController.swift */; }; B5A391B124E96AF600E7E8BD /* Modern.PokedexDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391B024E96AF600E7E8BD /* Modern.PokedexDemo.swift */; }; - B5A391B424E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift */; }; + B5A391B424E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonDisplay.swift */; }; B5A391B624E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391B524E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift */; }; B5A391B924E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391B824E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift */; }; B5A391BB24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391BA24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift */; }; @@ -91,6 +92,7 @@ B531EFEA24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.Service.swift; sourceTree = ""; }; B531EFEC24EB7453005F247D /* Modern.PokedexDemo.MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.MainView.swift; sourceTree = ""; }; B54269C524ED190C00A66C23 /* Modern.PokedexDemo.ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.ItemView.swift; sourceTree = ""; }; + B566C8E524ED6B98001134A1 /* NetworkImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkImageView.swift; sourceTree = ""; }; B5A3911924E5429200E7E8BD /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; B5A3911C24E5429200E7E8BD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -139,7 +141,7 @@ B5A391AB24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.UIKit.DetailView.swift; sourceTree = ""; }; B5A391AD24E9150F00E7E8BD /* Modern.ColorsDemo.UIKit.DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.UIKit.DetailViewController.swift; sourceTree = ""; }; B5A391B024E96AF600E7E8BD /* Modern.PokedexDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.swift; sourceTree = ""; }; - B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokemonSpecies.swift; sourceTree = ""; }; + B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokemonDisplay.swift; sourceTree = ""; }; B5A391B524E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.Move.swift; sourceTree = ""; }; B5A391B824E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokemonForm.swift; sourceTree = ""; }; B5A391BA24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokemonType.swift; sourceTree = ""; }; @@ -300,6 +302,7 @@ children = ( B5A3917B24E6A76C00E7E8BD /* LazyView.swift */, B5A3917F24E787D900E7E8BD /* InstructionsView.swift */, + B566C8E524ED6B98001134A1 /* NetworkImageView.swift */, B5A391A724E90F1000E7E8BD /* UIImage+Extensions.swift */, B5A3915424E6857F00E7E8BD /* Menu */, ); @@ -387,8 +390,8 @@ isa = PBXGroup; children = ( B531EFE824EB5A52005F247D /* Modern.PokedexDemo.PokedexEntry.swift */, - B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift */, B5A391B824E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift */, + B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonDisplay.swift */, B5A391B524E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift */, B5A391BC24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift */, B5A391B724E96E8600E7E8BD /* Attributes */, @@ -490,6 +493,7 @@ B5A3915E24E6922E00E7E8BD /* Modern.PlacemarksDemo.swift in Sources */, B5A391B124E96AF600E7E8BD /* Modern.PokedexDemo.swift in Sources */, B5A3918324E7A21800E7E8BD /* Modern.TimeZonesDemo.swift in Sources */, + B566C8E624ED6B98001134A1 /* NetworkImageView.swift in Sources */, B531EFEB24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift in Sources */, B5A3919824E7E67000E7E8BD /* Modern.ColorsDemo.Filter.swift in Sources */, B5A391A624E8F4EA00E7E8BD /* Modern.ColorsDemo.MainView.swift in Sources */, @@ -505,7 +509,7 @@ B54269C624ED190C00A66C23 /* Modern.PokedexDemo.ItemView.swift in Sources */, B531EFE924EB5A53005F247D /* Modern.PokedexDemo.PokedexEntry.swift in Sources */, B5A391B924E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift in Sources */, - B5A391B424E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift in Sources */, + B5A391B424E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonDisplay.swift in Sources */, B5A391BB24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift in Sources */, B5A3918C24E7B44B00E7E8BD /* Modern.TimeZonesDemo.ItemView.swift in Sources */, B5A3918A24E7AD1800E7E8BD /* Modern.TimeZonesDemo.ListView.swift in Sources */, diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Place.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Place.swift index 3054475..cac63b8 100644 --- a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Place.swift +++ b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Place.swift @@ -32,12 +32,7 @@ extension Modern.PlacemarksDemo { "annotation", customGetter: { object, field in - Modern.PlacemarksDemo.Place.Annotation( - latitude: object.$latitude.value, - longitude: object.$longitude.value, - title: object.$title.value, - subtitle: object.$subtitle.value - ) + Modern.PlacemarksDemo.Place.Annotation(object) }, customSetter: { object, field, newValue in @@ -115,6 +110,16 @@ extension Modern.PlacemarksDemo { self.title = title self.subtitle = subtitle } + + fileprivate init(_ object: ObjectProxy) { + + self.coordinate = .init( + latitude: object.$latitude.value, + longitude: object.$longitude.value + ) + self.title = object.$title.value + self.subtitle = object.$subtitle.value + } } } } diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ItemView.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ItemView.swift index 257a6d6..0f7d347 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ItemView.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ItemView.swift @@ -32,22 +32,52 @@ extension Modern.PokedexDemo { var body: some View { let pokedexEntry = self.pokedexEntry.snapshot - let form = pokedexEntry?.$form - let placeholderColor = Color.init(.sRGB, white: 0.95, opacity: 1) + let pokemonForm = pokedexEntry?.$pokemonForm?.snapshot + let pokemonDisplay = pokemonForm?.$pokemonDisplay?.snapshot + return HStack(spacing: 10) { - placeholderColor - .frame(width: 70, height: 70) - .cornerRadius(10) + + LazyView { + + NetworkImageView(url: pokemonDisplay?.$spriteURL) + .frame(width: 70, height: 70) + .id(pokemonDisplay) + } + ZStack { + + if let pokemonForm = pokemonForm { - Text(form?.$name ?? pokedexEntry?.$id ?? "") - .foregroundColor(form == nil ? placeholderColor : .init(.darkText)) - .fontWeight(form == nil ? .heavy : .regular) - .frame(maxWidth: .infinity) + VStack(alignment: .leading) { + + HStack { + Text(pokemonDisplay?.$displayName ?? pokemonForm.$name) + Spacer() + } + HStack { + self.view(for: pokemonForm.$pokemonType1) + if let pokemonType2 = pokemonForm.$pokemonType2 { + + self.view(for: pokemonType2) + } + Spacer() + } + Spacer() + } + } + else { + + Text(pokedexEntry?.$id ?? "") + .foregroundColor(Color(UIColor.placeholderText)) + .fontWeight(.heavy) + .frame(maxWidth: .infinity) + } + } + .frame(maxWidth: .infinity) } .padding() .onAppear { - if let pokedexEntry = pokedexEntry, form == nil { + if let pokedexEntry = pokedexEntry { self.service.fetchPokemonForm(for: pokedexEntry) } @@ -61,6 +91,19 @@ extension Modern.PokedexDemo { private var pokedexEntry: ObjectPublisher private let service: Modern.PokedexDemo.Service + + private func view(for pokemonType: Modern.PokedexDemo.PokemonType) -> some View { + ZStack { + Color(pokemonType.color) + .cornerRadius(5) + Text(pokemonType.rawValue) + .font(.subheadline) + .fontWeight(.bold) + .foregroundColor(.white) + .padding(.horizontal, 5) + .padding(.vertical, 2) + } + } } } @@ -82,7 +125,7 @@ struct _Demo_Modern_PokedexDemo_ItemView_Preview: PreviewProvider { } let pokedexEntry = transaction.create(Into()) pokedexEntry.id = "bulbasaur" - pokedexEntry.url = URL(string: "https://pokeapi.co/api/v2/pokemon/1/")! + pokedexEntry.pokemonFormURL = URL(string: "https://pokeapi.co/api/v2/pokemon/1/")! } ) diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift index ea5fceb..66cdec0 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift @@ -83,7 +83,6 @@ extension Modern.PokedexDemo { } } } - .id(pokedexEntries) } } .navigationBarTitle("Pokedex") diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift index e618bdc..5f1c8c8 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift @@ -21,14 +21,20 @@ extension Modern.PokedexDemo { var id: String = "" @Field.Stored( - "url", + "pokemonFormURL", dynamicInitialValue: { URL(string: "data:application/json,%7B%7D")! } ) - var url: URL + var pokemonFormURL: URL + + @Field.Stored( + "updateHash", + dynamicInitialValue: { UUID() } + ) + var updateHash: UUID - @Field.Relationship("form") - var form: Modern.PokedexDemo.PokemonForm? + @Field.Relationship("pokemonForm") + var pokemonForm: Modern.PokedexDemo.PokemonForm? // MARK: ImportableObject @@ -48,7 +54,7 @@ extension Modern.PokedexDemo { set { self.id = newValue } } - static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> String? { + static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType? { let json = source.json return try Modern.PokedexDemo.Service.parseJSON(json["name"]) @@ -58,7 +64,7 @@ extension Modern.PokedexDemo { let json = source.json self.index = source.index - self.url = try Modern.PokedexDemo.Service.parseJSON(json["url"], transformer: URL.init(string:)) + self.pokemonFormURL = try Modern.PokedexDemo.Service.parseJSON(json["url"], transformer: URL.init(string:)) } } } diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonDisplay.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonDisplay.swift new file mode 100644 index 0000000..ed69659 --- /dev/null +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonDisplay.swift @@ -0,0 +1,83 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import UIKit + +// MARK: - Modern.PokedexDemo + +extension Modern.PokedexDemo { + + // MARK: - Modern.PokedexDemo.PokemonDisplay + + final class PokemonDisplay: CoreStoreObject, ImportableUniqueObject { + + // MARK: Internal + + @Field.Stored("id") + var id: Int = 0 + + @Field.Stored("displayName") + var displayName: String? + + @Field.Stored("spriteURL") + var spriteURL: URL? + + + @Field.Relationship("form") + var pokedexForm: Modern.PokedexDemo.PokemonForm? + + + // MARK: ImportableObject + + typealias ImportSource = Dictionary + + + // MARK: ImportableUniqueObject + + typealias UniqueIDType = Int + + static let uniqueIDKeyPath: String = String(keyPath: \Modern.PokedexDemo.PokemonDisplay.$id) + + var uniqueIDValue: UniqueIDType { + + get { return self.id } + set { self.id = newValue } + } + + static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType? { + + let json = source + return try Modern.PokedexDemo.Service.parseJSON(json["id"]) + } + + func update(from source: ImportSource, in transaction: BaseDataTransaction) throws { + + typealias Service = Modern.PokedexDemo.Service + let json = source + + for json in try Service.parseJSON(json["names"]) as [Dictionary] { + + let displayName: String = try Service.parseJSON(json["name"]) + let language: String = try Service.parseJSON( + json["language"], + transformer: { (json: Dictionary) in + try Service.parseJSON(json["name"]) + } + ) + switch language { + + case "en": self.displayName = displayName + default: break + } + } + self.spriteURL = try Service.parseJSON( + json["sprites"], + transformer: { (json: Dictionary) in + try Service.parseJSON(json["front_default"], transformer: URL.init(string:)) + } + ) + } + } +} diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonForm.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonForm.swift index 49626d5..c277653 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonForm.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonForm.swift @@ -10,7 +10,7 @@ extension Modern.PokedexDemo { // MARK: - Modern.PokedexDemo.PokemonForm - final class PokemonForm: CoreStoreObject { + final class PokemonForm: CoreStoreObject, ImportableUniqueObject { // MARK: Internal @@ -28,9 +28,6 @@ extension Modern.PokedexDemo { @Field.Stored("pokemonType2") var pokemonType2: Modern.PokedexDemo.PokemonType? - - @Field.Stored("spriteURL") - var spriteURL: URL? @Field.Stored("statHitPoints") @@ -52,6 +49,16 @@ extension Modern.PokedexDemo { var statSpeed: Int = 0 + @Field.Stored( + "pokemonDisplayURL", + dynamicInitialValue: { URL(string: "data:application/json,%7B%7D")! } + ) + var pokemonDisplayURL: URL + + + @Field.Relationship("display", inverse: \.$pokedexForm) + var pokemonDisplay: Modern.PokedexDemo.PokemonDisplay? + @Field.Relationship("abilities", inverse: \.$learners) var abilities: Set @@ -59,10 +66,99 @@ extension Modern.PokedexDemo { var moves: Set - @Field.Relationship("pokedexEntry", inverse: \.$form) + @Field.Relationship("pokedexEntry", inverse: \.$pokemonForm) var pokedexEntry: Modern.PokedexDemo.PokedexEntry? - @Field.Relationship("species", inverse: \.$forms) - var species: Modern.PokedexDemo.PokemonSpecies? + + // MARK: ImportableObject + + typealias ImportSource = Dictionary + + + // MARK: ImportableUniqueObject + + typealias UniqueIDType = Int + + static let uniqueIDKeyPath: String = String(keyPath: \Modern.PokedexDemo.PokemonForm.$id) + + var uniqueIDValue: UniqueIDType { + + get { return self.id } + set { self.id = newValue } + } + + static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType? { + + let json = source + return try Modern.PokedexDemo.Service.parseJSON(json["id"]) + } + + func update(from source: ImportSource, in transaction: BaseDataTransaction) throws { + + typealias Service = Modern.PokedexDemo.Service + let json = source + + self.name = try Service.parseJSON(json["name"]) + self.weight = try Service.parseJSON(json["weight"]) + + for json in try Service.parseJSON(json["types"]) as [Dictionary] { + + let slot: Int = try Service.parseJSON(json["slot"]) + let pokemonType = try Service.parseJSON( + json["type"], + transformer: { (json: Dictionary) in + Modern.PokedexDemo.PokemonType(rawValue: try Service.parseJSON(json["name"])) + } + ) + switch slot { + + case 1: self.pokemonType1 = pokemonType + case 2: self.pokemonType2 = pokemonType + default: continue + } + } + + for json in try Service.parseJSON(json["stats"]) as [Dictionary] { + + let baseStat: Int = try Service.parseJSON(json["base_stat"]) + let name: String = try Service.parseJSON( + json["stat"], + transformer: { (json: Dictionary) in + try Service.parseJSON(json["name"]) + } + ) + switch name { + + case "hp": self.statHitPoints = baseStat + case "attack": self.statAttack = baseStat + case "defense": self.statDefense = baseStat + case "special-attack": self.statSpecialAttack = baseStat + case "special-defense": self.statSpecialDefense = baseStat + case "speed": self.statSpeed = baseStat + default: continue + } + } + + do { + + let abilities: [Dictionary] = try Service.parseJSON(json["abilities"]) + } + do { + + let moves: [Dictionary] = try Service.parseJSON(json["moves"]) + } + + for json in try Service.parseJSON(json["forms"]) as [Dictionary] { + + let name: String = try Service.parseJSON(json["name"]) + let pokemonDisplayURL = try Service.parseJSON(json["url"], transformer: URL.init(string:)) + + guard name == self.name else { + + continue + } + self.pokemonDisplayURL = pokemonDisplayURL + } + } } } diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonSpecies.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonSpecies.swift deleted file mode 100644 index 067db1f..0000000 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonSpecies.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Demo -// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. - -import CoreStore - -// MARK: - Modern.PokedexDemo - -extension Modern.PokedexDemo { - - // MARK: - Modern.PokedexDemo.PokemonSpecies - - final class PokemonSpecies: CoreStoreObject { - - // MARK: Internal - - @Field.Stored("id") - var id: Int = 0 - - @Field.Stored("name") - var name: String = "" - - @Field.Stored("weight") - var weight: Int = 0 - - - @Field.Relationship("forms") - var forms: Set - } -} diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonType.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonType.swift index f7546bf..8e078f2 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonType.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonType.swift @@ -3,6 +3,7 @@ // Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. import CoreStore +import UIKit // MARK: - Modern.PokedexDemo @@ -32,5 +33,30 @@ extension Modern.PokedexDemo { case rock case steel case water + + var color: UIColor { + + switch self { + + case .bug: return #colorLiteral(red: 0.568627450980392, green: 0.749019607843137, blue: 0.231372549019608, alpha: 1.0) // #91BF3B, a: 1.0 + case .dark: return #colorLiteral(red: 0.392156862745098, green: 0.388235294117647, blue: 0.454901960784314, alpha: 1.0) // #646374, a: 1.0 + case .dragon: return #colorLiteral(red: 0.0823529411764706, green: 0.423529411764706, blue: 0.741176470588235, alpha: 1.0) // #156CBD, a: 1.0 + case .electric: return #colorLiteral(red: 0.949019607843137, green: 0.819607843137255, blue: 0.298039215686275, alpha: 1.0) // #F2D14C, a: 1.0 + case .fairy: return #colorLiteral(red: 0.913725490196078, green: 0.56078431372549, blue: 0.882352941176471, alpha: 1.0) // #E98FE1, a: 1.0 + case .fighting: return #colorLiteral(red: 0.8, green: 0.254901960784314, blue: 0.423529411764706, alpha: 1.0) // #CC416C, a: 1.0 + case .fire: return #colorLiteral(red: 0.992156862745098, green: 0.607843137254902, blue: 0.352941176470588, alpha: 1.0) // #FD9B5A, a: 1.0 + case .flying: return #colorLiteral(red: 0.619607843137255, green: 0.701960784313725, blue: 0.886274509803922, alpha: 1.0) // #9EB3E2, a: 1.0 + case .ghost: return #colorLiteral(red: 0.333333333333333, green: 0.419607843137255, blue: 0.670588235294118, alpha: 1.0) // #556BAB, a: 1.0 + case .grass: return #colorLiteral(red: 0.38823529411764707, green: 0.7215686274509804, blue: 0.3803921568627451, alpha: 1.0) // #63B861, a: 1.0 + case .ground: return #colorLiteral(red: 0.847058823529412, green: 0.458823529411765, blue: 0.298039215686275, alpha: 1.0) // #D8754C, a: 1.0 + case .ice: return #colorLiteral(red: 0.466666666666667, green: 0.803921568627451, blue: 0.756862745098039, alpha: 1.0) // #77CDC1, a: 1.0 + case .normal: return #colorLiteral(red: 0.564705882352941, green: 0.603921568627451, blue: 0.627450980392157, alpha: 1.0) // #909AA0, a: 1.0 + case .poison: return #colorLiteral(red: 0.647058823529412, green: 0.411764705882353, blue: 0.768627450980392, alpha: 1.0) // #A569C4, a: 1.0 + case .psychic: return #colorLiteral(red: 0.9764705882, green: 0.5058823529, blue: 0.5019607843, alpha: 1) // #F98180, a: 1.0 + case .rock: return #colorLiteral(red: 0.776470588235294, green: 0.717647058823529, blue: 0.556862745098039, alpha: 1.0) // #C6B78E, a: 1.0 + case .steel: return #colorLiteral(red: 0.329411764705882, green: 0.529411764705882, blue: 0.607843137254902, alpha: 1.0) // #54879B, a: 1.0 + case .water: return #colorLiteral(red: 0.325490196078431, green: 0.576470588235294, blue: 0.823529411764706, alpha: 1.0) // #5393D2, a: 1.0 + } + } } } diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift index 959f4b7..933f228 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift @@ -5,6 +5,7 @@ import Foundation import Combine import CoreStore +import UIKit // MARK: - Modern.PokedexDemo @@ -35,7 +36,11 @@ extension Modern.PokedexDemo { init() {} - static func parseJSON(_ json: Any?) throws -> Output { + static func parseJSON( + _ json: Any?, + file: StaticString = #file, + line: Int = #line + ) throws -> Output { switch json { @@ -45,30 +50,38 @@ extension Modern.PokedexDemo { case let any: throw Modern.PokedexDemo.Service.Error.parseError( expected: Output.self, - actual: type(of: any) + actual: type(of: any), + file: "\(file):\(line)" ) } } - static func parseJSON(_ json: Any?, transformer: (JSONType) -> Output?) throws -> Output { + static func parseJSON( + _ json: Any?, + transformer: (JSONType) throws -> Output?, + file: StaticString = #file, + line: Int = #line + ) throws -> Output { switch json { case let json as JSONType: - let transformed = transformer(json) + let transformed = try transformer(json) if let json = transformed { return json } throw Modern.PokedexDemo.Service.Error.parseError( expected: Output.self, - actual: type(of: transformed) + actual: type(of: transformed), + file: "\(file):\(line)" ) case let any: throw Modern.PokedexDemo.Service.Error.parseError( expected: Output.self, - actual: type(of: any) + actual: type(of: any), + file: "\(file):\(line)" ) } } @@ -76,6 +89,7 @@ extension Modern.PokedexDemo { func fetchPokedexEntries() { self.cancellable["pokedexEntries"] = self.pokedexEntries + .receive(on: DispatchQueue.main) .handleEvents( receiveSubscription: { [weak self] _ in @@ -101,6 +115,7 @@ extension Modern.PokedexDemo { self.lastError = nil case .failure(let error): + print(error) self.lastError = ( error: error, retry: { [weak self] in @@ -116,44 +131,157 @@ extension Modern.PokedexDemo { func fetchPokemonForm(for pokedexEntry: ObjectSnapshot) { + if let pokedexForm = pokedexEntry.$pokemonForm?.snapshot { + + self.fetchPokemonDisplay(for: pokedexForm) + return + } self.cancellable["pokemonForm.\(pokedexEntry.$id)"] = URLSession.shared - .dataTaskPublisher(for: pokedexEntry.$url) - .eraseToAnyPublisher() + .dataTaskPublisher(for: pokedexEntry.$pokemonFormURL) + .mapError({ .networkError($0) }) + .flatMap( + { output in + + return Future, Modern.PokedexDemo.Service.Error> { promise in + + Modern.PokedexDemo.dataStack.perform( + asynchronous: { transaction -> Modern.PokedexDemo.PokemonForm in + + let json: Dictionary = try Self.parseJSON( + try JSONSerialization.jsonObject(with: output.data, options: []) + ) + guard let pokedexForm = try transaction.importUniqueObject( + Into(), + source: json + ) else { + + throw Modern.PokedexDemo.Service.Error.unexpected + } + if let pokedexEntry = pokedexEntry.asEditable(in: transaction) { + + pokedexForm.pokedexEntry = pokedexEntry + pokedexEntry.updateHash = .init() + } + return pokedexForm + }, + success: { pokemonForm in + + promise(.success(pokemonForm.asSnapshot(in: Modern.PokedexDemo.dataStack)!)) + }, + failure: { error in + + switch error { + + case .userError(let error): + switch error { + + case let error as Modern.PokedexDemo.Service.Error: + promise(.failure(error)) + + case let error: + promise(.failure(.otherError(error))) + } + + case let error: + promise(.failure(.saveError(error))) + } + } + ) + } + } + ) .sink( - receiveCompletion: { _ in }, + receiveCompletion: { completion in + + switch completion { + + case .finished: + break + + case .failure(let error): + print(error) + } + }, + receiveValue: { pokemonForm in + + self.fetchPokemonDisplay(for: pokemonForm) + } + ) + } + + func fetchPokemonDisplay(for pokemonForm: ObjectSnapshot) { + + if let pokemonDisplay = pokemonForm.$pokemonDisplay?.snapshot { + + return + } + self.cancellable["pokemonDisplay.\(pokemonForm.$id)"] = URLSession.shared + .dataTaskPublisher(for: pokemonForm.$pokemonDisplayURL) + .mapError({ .networkError($0) }) + .flatMap( + { output in + + return Future { promise in + + Modern.PokedexDemo.dataStack.perform( + asynchronous: { transaction -> Void in + + let json: Dictionary = try Self.parseJSON( + try JSONSerialization.jsonObject(with: output.data, options: []) + ) + guard let pokemonDisplay = try transaction.importUniqueObject( + Into(), + source: json + ) else { + + throw Modern.PokedexDemo.Service.Error.unexpected + } + if let pokemonForm = pokemonForm.asEditable(in: transaction) { + + pokemonDisplay.pokedexForm = pokemonForm + pokemonForm.pokedexEntry?.updateHash = .init() + } + }, + success: { + + promise(.success(())) + }, + failure: { error in + + switch error { + + case .userError(let error): + switch error { + + case let error as Modern.PokedexDemo.Service.Error: + promise(.failure(error)) + + case let error: + promise(.failure(.otherError(error))) + } + + case let error: + promise(.failure(.saveError(error))) + } + } + ) + } + } + ) + .sink( + receiveCompletion: { completion in + + switch completion { + + case .finished: + break + + case .failure(let error): + print(error) + } + }, receiveValue: { output in -// do { -// -// let json: Dictionary = try Self.parseJSON( -// try JSONSerialization.jsonObject(with: output.data, options: []) -// ) -// let results: [Dictionary] = try Self.parseJSON( -// json["results"] -// ) -// Modern.PokedexDemo.dataStack.perform( -// asynchronous: { transaction -> Void in -// -// _ = try transaction.importUniqueObjects( -// Into(), -// sourceArray: results.enumerated().map { (index, json) in -// (index: index, json: json) -// } -// ) -// }, -// success: { result in -// -// promise(.success(result)) -// }, -// failure: { error in -// -// promise(.failure(.saveError(error))) -// } -// ) -// } -// catch { -// -// } } ) } @@ -212,7 +340,6 @@ extension Modern.PokedexDemo { } } ) - .receive(on: DispatchQueue.main) .eraseToAnyPublisher() @@ -221,9 +348,10 @@ extension Modern.PokedexDemo { enum Error: Swift.Error { case networkError(URLError) - case parseError(expected: Any.Type, actual: Any.Type) + case parseError(expected: Any.Type, actual: Any.Type, file: String) case saveError(CoreStoreError) case otherError(Swift.Error) + case unexpected } } } diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift index f238b9c..76b6e36 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift @@ -24,8 +24,8 @@ extension Modern { modelVersion: "V1", entities: [ Entity("PokedexEntry"), - Entity("PokemonSpecies"), Entity("PokemonForm"), + Entity("PokemonDisplay"), Entity("Move"), Entity("Ability") ] diff --git a/Demo/Sources/Helpers/NetworkImageView.swift b/Demo/Sources/Helpers/NetworkImageView.swift new file mode 100644 index 0000000..233fdec --- /dev/null +++ b/Demo/Sources/Helpers/NetworkImageView.swift @@ -0,0 +1,107 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import Combine +import SwiftUI + +// MARK: - NetworkImageView + +struct NetworkImageView: View { + + // MARK: Internal + + init(url: URL?) { + + self.imageDownloader = .init(url: url) + } + + + // MARK: View + + var body: some View { + if let image = self.imageDownloader.image { + + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + } + else { + + Circle() + .colorMultiply(Color(UIColor.placeholderText)) + .onAppear { + + self.imageDownloader.fetchImage() + } + } + } + + + // MARK: Private + + @ObservedObject + private var imageDownloader: ImageDownloader + + + // MARK: - NetworkImageView.ImageDownloader + + fileprivate final class ImageDownloader: ObservableObject { + + // MARK: FilePrivate + + private(set) var image: UIImage? + + let url: URL? + + init(url: URL?) { + + self.url = url + guard let url = url else { + + return + } + if let image = Self.cache[url] { + + self.image = image + } + } + + func fetchImage() { + + guard let url = url else { + + return + } + if let image = Self.cache[url] { + + self.objectWillChange.send() + self.image = image + return + } + self.cancellable = URLSession.shared + .dataTaskPublisher(for: url) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in }, + receiveValue: { output in + + if let image = UIImage(data: output.data) { + + Self.cache[url] = image + self.objectWillChange.send() + self.image = image + } + } + ) + } + + + // MARK: Private + + private static var cache: [URL: UIImage] = [:] + + private var cancellable: AnyCancellable? + } +} +