pokedex demo

This commit is contained in:
John Estropia
2020-08-20 00:39:03 +09:00
parent 2c0cadf2fa
commit 8b3b947406
12 changed files with 575 additions and 108 deletions

View File

@@ -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 = "<group>"; };
B531EFEC24EB7453005F247D /* Modern.PokedexDemo.MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.MainView.swift; sourceTree = "<group>"; };
B54269C524ED190C00A66C23 /* Modern.PokedexDemo.ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.ItemView.swift; sourceTree = "<group>"; };
B566C8E524ED6B98001134A1 /* NetworkImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkImageView.swift; sourceTree = "<group>"; };
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 = "<group>"; };
B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
@@ -139,7 +141,7 @@
B5A391AB24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.UIKit.DetailView.swift; sourceTree = "<group>"; };
B5A391AD24E9150F00E7E8BD /* Modern.ColorsDemo.UIKit.DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.UIKit.DetailViewController.swift; sourceTree = "<group>"; };
B5A391B024E96AF600E7E8BD /* Modern.PokedexDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.swift; sourceTree = "<group>"; };
B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokemonSpecies.swift; sourceTree = "<group>"; };
B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokemonDisplay.swift; sourceTree = "<group>"; };
B5A391B524E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.Move.swift; sourceTree = "<group>"; };
B5A391B824E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokemonForm.swift; sourceTree = "<group>"; };
B5A391BA24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokemonType.swift; sourceTree = "<group>"; };
@@ -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 */,

View File

@@ -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<Modern.PlacemarksDemo.Place>) {
self.coordinate = .init(
latitude: object.$latitude.value,
longitude: object.$longitude.value
)
self.title = object.$title.value
self.subtitle = object.$subtitle.value
}
}
}
}

View File

@@ -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<Modern.PokedexDemo.PokedexEntry>
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<Modern.PokedexDemo.PokedexEntry>())
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/")!
}
)

View File

@@ -83,7 +83,6 @@ extension Modern.PokedexDemo {
}
}
}
.id(pokedexEntries)
}
}
.navigationBarTitle("Pokedex")

View File

@@ -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:))
}
}
}

View File

@@ -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<String, Any>
// 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<String, Any>] {
let displayName: String = try Service.parseJSON(json["name"])
let language: String = try Service.parseJSON(
json["language"],
transformer: { (json: Dictionary<String, Any>) 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<String, Any>) in
try Service.parseJSON(json["front_default"], transformer: URL.init(string:))
}
)
}
}
}

View File

@@ -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<Modern.PokedexDemo.Ability>
@@ -59,10 +66,99 @@ extension Modern.PokedexDemo {
var moves: Set<Modern.PokedexDemo.Move>
@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<String, Any>
// 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<String, Any>] {
let slot: Int = try Service.parseJSON(json["slot"])
let pokemonType = try Service.parseJSON(
json["type"],
transformer: { (json: Dictionary<String, Any>) 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<String, Any>] {
let baseStat: Int = try Service.parseJSON(json["base_stat"])
let name: String = try Service.parseJSON(
json["stat"],
transformer: { (json: Dictionary<String, Any>) 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<String, Any>] = try Service.parseJSON(json["abilities"])
}
do {
let moves: [Dictionary<String, Any>] = try Service.parseJSON(json["moves"])
}
for json in try Service.parseJSON(json["forms"]) as [Dictionary<String, Any>] {
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
}
}
}
}

View File

@@ -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<Modern.PokedexDemo.PokemonForm>
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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<Output>(_ json: Any?) throws -> Output {
static func parseJSON<Output>(
_ 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<JSONType, Output>(_ json: Any?, transformer: (JSONType) -> Output?) throws -> Output {
static func parseJSON<JSONType, Output>(
_ 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<Modern.PokedexDemo.PokedexEntry>) {
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<ObjectSnapshot<Modern.PokedexDemo.PokemonForm>, Modern.PokedexDemo.Service.Error> { promise in
Modern.PokedexDemo.dataStack.perform(
asynchronous: { transaction -> Modern.PokedexDemo.PokemonForm in
let json: Dictionary<String, Any> = try Self.parseJSON(
try JSONSerialization.jsonObject(with: output.data, options: [])
)
guard let pokedexForm = try transaction.importUniqueObject(
Into<Modern.PokedexDemo.PokemonForm>(),
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<Modern.PokedexDemo.PokemonForm>) {
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<Void, Modern.PokedexDemo.Service.Error> { promise in
Modern.PokedexDemo.dataStack.perform(
asynchronous: { transaction -> Void in
let json: Dictionary<String, Any> = try Self.parseJSON(
try JSONSerialization.jsonObject(with: output.data, options: [])
)
guard let pokemonDisplay = try transaction.importUniqueObject(
Into<Modern.PokedexDemo.PokemonDisplay>(),
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<String, Any> = try Self.parseJSON(
// try JSONSerialization.jsonObject(with: output.data, options: [])
// )
// let results: [Dictionary<String, Any>] = try Self.parseJSON(
// json["results"]
// )
// Modern.PokedexDemo.dataStack.perform(
// asynchronous: { transaction -> Void in
//
// _ = try transaction.importUniqueObjects(
// Into<Modern.PokedexDemo.PokedexEntry>(),
// 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
}
}
}

View File

@@ -24,8 +24,8 @@ extension Modern {
modelVersion: "V1",
entities: [
Entity<Modern.PokedexDemo.PokedexEntry>("PokedexEntry"),
Entity<Modern.PokedexDemo.PokemonSpecies>("PokemonSpecies"),
Entity<Modern.PokedexDemo.PokemonForm>("PokemonForm"),
Entity<Modern.PokedexDemo.PokemonDisplay>("PokemonDisplay"),
Entity<Modern.PokedexDemo.Move>("Move"),
Entity<Modern.PokedexDemo.Ability>("Ability")
]

View File

@@ -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?
}
}