added demo for classic ListMonitor

This commit is contained in:
John Estropia
2020-08-30 20:16:01 +09:00
parent 007da014f8
commit 8d7f282743
63 changed files with 1463 additions and 102 deletions

View File

@@ -1,27 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.Details
final class Details: CoreStoreObject {
// MARK: Internal
@Field.Relationship("pokedexEntry", inverse: \.$details)
var pokedexEntry: Modern.PokedexDemo.PokedexEntry?
@Field.Relationship("species")
var species: Modern.PokedexDemo.Species?
@Field.Relationship("forms")
var forms: [Modern.PokedexDemo.Form]
}
}

View File

@@ -1,387 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Combine
import CoreStore
import UIKit
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.ItemCell
final class ItemCell: UICollectionViewCell {
// MARK: Internal
static let reuseIdentifier: String = NSStringFromClass(Modern.PokedexDemo.ItemCell.self)
func setPokedexEntry(
_ pokedexEntry: Modern.PokedexDemo.PokedexEntry,
service: Modern.PokedexDemo.Service
) {
guard let pokedexEntry = pokedexEntry.asPublisher() else {
return self.pokedexEntry = nil
}
guard self.pokedexEntry != pokedexEntry else {
return
}
self.service = service
self.pokedexEntry = pokedexEntry
self.didUpdateData(animated: false)
if let snapshot = pokedexEntry.snapshot {
service.fetchDetails(for: snapshot)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError()
}
// MARK: UITableViewCell
override init(frame: CGRect) {
super.init(frame: frame)
let contentView = self.contentView
do {
contentView.backgroundColor = UIColor.placeholderText.withAlphaComponent(0.1)
contentView.layer.cornerRadius = 10
contentView.layer.masksToBounds = true
}
let typesContainerView = UIStackView()
do {
typesContainerView.translatesAutoresizingMaskIntoConstraints = false
typesContainerView.axis = .horizontal
typesContainerView.alignment = .fill
typesContainerView.distribution = .fillEqually
typesContainerView.spacing = 0
typesContainerView.addArrangedSubview(self.type1View)
typesContainerView.addArrangedSubview(self.type2View)
contentView.addSubview(typesContainerView)
}
let spriteView = self.spriteView
do {
spriteView.translatesAutoresizingMaskIntoConstraints = false
spriteView.contentMode = .scaleAspectFill
contentView.addSubview(spriteView)
}
let placeholderLabel = self.placeholderLabel
do {
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
placeholderLabel.textColor = UIColor.placeholderText
placeholderLabel.font = UIFont.systemFont(ofSize: 20, weight: .heavy)
placeholderLabel.numberOfLines = 0
placeholderLabel.textAlignment = .center
contentView.addSubview(placeholderLabel)
}
let nameLabel = self.nameLabel
do {
nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.textColor = UIColor.white
nameLabel.font = UIFont.systemFont(ofSize: 14, weight: .bold)
nameLabel.numberOfLines = 0
nameLabel.textAlignment = .center
contentView.addSubview(nameLabel)
}
layout: do {
NSLayoutConstraint.activate(
[
typesContainerView.topAnchor.constraint(
equalTo: contentView.topAnchor
),
typesContainerView.leadingAnchor.constraint(
equalTo: contentView.leadingAnchor
),
typesContainerView.bottomAnchor.constraint(
equalTo: contentView.bottomAnchor
),
typesContainerView.trailingAnchor.constraint(
equalTo: contentView.trailingAnchor
),
spriteView.topAnchor.constraint(
equalTo: contentView.topAnchor
),
spriteView.leadingAnchor.constraint(
equalTo: contentView.leadingAnchor
),
spriteView.bottomAnchor.constraint(
equalTo: contentView.bottomAnchor
),
spriteView.trailingAnchor.constraint(
equalTo: contentView.trailingAnchor
),
placeholderLabel.topAnchor.constraint(
equalTo: contentView.topAnchor,
constant: 10
),
placeholderLabel.leadingAnchor.constraint(
equalTo: contentView.leadingAnchor,
constant: 10
),
placeholderLabel.bottomAnchor.constraint(
equalTo: contentView.bottomAnchor,
constant: -10
),
placeholderLabel.trailingAnchor.constraint(
equalTo: contentView.trailingAnchor,
constant: -10
),
nameLabel.leadingAnchor.constraint(
equalTo: contentView.leadingAnchor,
constant: 10
),
nameLabel.bottomAnchor.constraint(
equalTo: contentView.bottomAnchor,
constant: -10
),
nameLabel.trailingAnchor.constraint(
equalTo: contentView.trailingAnchor,
constant: -10
),
]
)
}
}
override func prepareForReuse() {
super.prepareForReuse()
self.service = nil
self.pokedexEntry = nil
self.didUpdateData(animated: false)
}
// MARK: Private
private let spriteView: UIImageView = .init()
private let placeholderLabel: UILabel = .init()
private let nameLabel: UILabel = .init()
private let type1View: UIView = .init()
private let type2View: UIView = .init()
private var service: Modern.PokedexDemo.Service?
private var imageURL: URL? {
didSet {
let newValue = self.imageURL
guard newValue != oldValue else {
return
}
self.imageDownloader = ImageDownloader(url: newValue)
}
}
private var imageDownloader: ImageDownloader = .init(url: nil) {
didSet {
let url = self.imageDownloader.url
if url == nil {
self.spriteView.image = nil
return
}
self.imageDownloader.fetchImage { [weak self] in
guard let self = self, url == self.imageURL else {
return
}
self.spriteView.image = $0
self.spriteView.layer.add(CATransition(), forKey: nil)
}
}
}
private var pokedexEntry: ObjectPublisher<Modern.PokedexDemo.PokedexEntry>? {
didSet {
let newValue = self.pokedexEntry
guard newValue != oldValue else {
return
}
oldValue?.removeObserver(self)
newValue?.addObserver(self) { [weak self] newValue in
guard let self = self else {
return
}
self.details = newValue.snapshot?.$details
self.didUpdateData(animated: true)
}
self.details = newValue?.snapshot?.$details
}
}
private var details: ObjectPublisher<Modern.PokedexDemo.Details>? {
didSet {
let newValue = self.details
guard newValue != oldValue else {
return
}
oldValue?.removeObserver(self)
newValue?.addObserver(self) { [weak self] newValue in
guard let self = self else {
return
}
let details = newValue.snapshot
self.species = details?.$species
self.forms = details?.$forms
self.didUpdateData(animated: true)
}
let details = newValue?.snapshot
self.species = details?.$species
self.forms = details?.$forms
}
}
private var species: ObjectPublisher<Modern.PokedexDemo.Species>? {
didSet {
let newValue = self.species
guard newValue != oldValue else {
return
}
oldValue?.removeObserver(self)
newValue?.addObserver(self) { [weak self] _ in
self?.didUpdateData(animated: true)
}
}
}
private var formsRotationCancellable: AnyCancellable?
private var forms: [ObjectPublisher<Modern.PokedexDemo.Form>]? {
didSet {
let newValue = self.forms
guard newValue != oldValue else {
return
}
self.currentForm = newValue?.first
self.formsRotationCancellable = newValue.flatMap { newValue in
guard !newValue.isEmpty else {
return nil
}
return Timer
.publish(every: 0.5, on: .main, in: .common)
.autoconnect()
.scan(1, { (index, _) in index + 1 })
.sink(
receiveValue: { [weak self] (index) in
guard let self = self else {
return
}
self.currentForm = newValue[index % newValue.count]
self.didUpdateData(animated: true)
}
)
}
}
}
private var currentForm: ObjectPublisher<Modern.PokedexDemo.Form>? {
didSet {
let newValue = self.currentForm
guard newValue != oldValue else {
return
}
oldValue?.removeObserver(self)
newValue?.addObserver(self) { [weak self] _ in
self?.didUpdateData(animated: true)
}
}
}
private func didUpdateData(animated: Bool) {
let pokedexEntry = self.pokedexEntry?.snapshot
let species = self.species?.snapshot
let currentForm = self.currentForm?.snapshot
self.placeholderLabel.text = pokedexEntry?.$id
self.placeholderLabel.isHidden = species != nil
self.type1View.backgroundColor = species?.$pokemonType1.color
?? UIColor.clear
self.type1View.isHidden = species == nil
self.type2View.backgroundColor = species?.$pokemonType2?.color
?? species?.$pokemonType1.color
?? UIColor.clear
self.type2View.isHidden = species == nil
self.nameLabel.text = currentForm?.$name ?? species?.$name
self.nameLabel.isHidden = currentForm == nil && species == nil
self.imageURL = currentForm?.$spriteURL
guard animated else {
return
}
self.contentView.layer.add(CATransition(), forKey: nil)
}
}
}

View File

@@ -1,72 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import SwiftUI
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.ListView
struct ListView: UIViewControllerRepresentable {
// MARK: Internal
init(
service: Modern.PokedexDemo.Service,
listPublisher: ListPublisher<Modern.PokedexDemo.PokedexEntry>
) {
self.service = service
self.listPublisher = listPublisher
}
// MARK: UIViewControllerRepresentable
typealias UIViewControllerType = Modern.PokedexDemo.ListViewController
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
return UIViewControllerType(
service: self.service,
listPublisher: self.listPublisher
)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {}
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
// MARK: Private
@ObservedObject
private var service: Modern.PokedexDemo.Service
private let listPublisher: ListPublisher<Modern.PokedexDemo.PokedexEntry>
}
}
#if DEBUG
struct _Demo_Modern_PokedexDemo_ListView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
let service = Modern.PokedexDemo.Service()
service.fetchPokedexEntries()
return Modern.PokedexDemo.ListView(
service: service,
listPublisher: Modern.PokedexDemo.pokedexEntries
)
}
}
#endif

View File

@@ -1,111 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.ListViewController
final class ListViewController: UICollectionViewController {
// MARK: Internal
init(
service: Modern.PokedexDemo.Service,
listPublisher: ListPublisher<Modern.PokedexDemo.PokedexEntry>
) {
self.service = service
self.listPublisher = listPublisher
let layout = UICollectionViewFlowLayout()
layout.sectionInset = .init(
top: 10, left: 10, bottom: 10, right: 10
)
layout.minimumInteritemSpacing = 10
layout.minimumLineSpacing = 10
let screenWidth = UIScreen.main.bounds.inset(by: layout.sectionInset).width
let cellsPerRow: CGFloat = 3
let cellWidth = min(
230,
floor((screenWidth - ((cellsPerRow - 1) * layout.minimumInteritemSpacing)) / cellsPerRow)
)
layout.itemSize = .init(
width: cellWidth,
height: ceil(cellWidth * (4 / 3))
)
super.init(collectionViewLayout: layout)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError()
}
deinit {
self.listPublisher.removeObserver(self)
}
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.backgroundColor = UIColor.systemBackground
self.collectionView.register(
Modern.PokedexDemo.ItemCell.self,
forCellWithReuseIdentifier: Modern.PokedexDemo.ItemCell.reuseIdentifier
)
self.startObservingList()
}
// MARK: Private
private let service: Modern.PokedexDemo.Service
private let listPublisher: ListPublisher<Modern.PokedexDemo.PokedexEntry>
private lazy var dataSource: DiffableDataSource.CollectionViewAdapter<Modern.PokedexDemo.PokedexEntry> = .init(
collectionView: self.collectionView,
dataStack: Modern.PokedexDemo.dataStack,
cellProvider: { (collectionView, indexPath, pokedexEntry) in
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: Modern.PokedexDemo.ItemCell.reuseIdentifier,
for: indexPath
) as! Modern.PokedexDemo.ItemCell
cell.setPokedexEntry(pokedexEntry, service: self.service)
return cell
}
)
private func startObservingList() {
self.listPublisher.addObserver(self) { (listPublisher) in
self.dataSource.apply(
listPublisher.snapshot,
animatingDifferences: true
)
}
self.dataSource.apply(
self.listPublisher.snapshot,
animatingDifferences: false
)
}
}
}

View File

@@ -1,89 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Combine
import CoreStore
import SwiftUI
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.MainView
struct MainView: View {
// MARK: Internal
init() {
self.pokedexEntries = Modern.PokedexDemo.pokedexEntries
}
// MARK: View
var body: some View {
let pokedexEntries = self.pokedexEntries.snapshot
return ZStack {
Modern.PokedexDemo.ListView(
service: self.service,
listPublisher: self.pokedexEntries
)
.frame(minHeight: 0, maxHeight: .infinity)
.edgesIgnoringSafeArea(.vertical)
if pokedexEntries.isEmpty {
VStack(alignment: .center, spacing: 30) {
Text("This demo needs to make a network connection to download Pokedex entries")
.multilineTextAlignment(.center)
if self.service.isLoading {
Text("Fetching Pokedex…")
}
else {
Button(
action: { self.service.fetchPokedexEntries() },
label: {
Text("Download Pokedex Entries")
}
)
}
}
.padding()
}
}
.navigationBarTitle("Pokedex")
}
// MARK: Private
@ObservedObject
private var pokedexEntries: ListPublisher<Modern.PokedexDemo.PokedexEntry>
@ObservedObject
private var service: Modern.PokedexDemo.Service = .init()
}
}
#if DEBUG
@available(iOS 14.0, *)
struct _Demo_Modern_PokedexDemo_MainView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Modern.PokedexDemo.MainView()
}
}
#endif

View File

@@ -1,57 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Modern
extension Modern {
// MARK: - Modern.PokedexDemo
/**
Sample usages for importing external data into `CoreStoreObject` attributes
*/
enum PokedexDemo {
// MARK: Internal
static let dataStack: DataStack = {
let dataStack = DataStack(
CoreStoreSchema(
modelVersion: "V1",
entities: [
Entity<Modern.PokedexDemo.PokedexEntry>("PokedexEntry"),
Entity<Modern.PokedexDemo.Details>("Details"),
Entity<Modern.PokedexDemo.Species>("Species"),
Entity<Modern.PokedexDemo.Form>("Form")
],
versionLock: [
"Details": [0x1cce0e9508eaa960, 0x74819067b54bd5c6, 0xc30c837f48811f10, 0x622bead2d27dea95],
"Form": [0x7cb78e58bbb79e3c, 0x149557c60be8427, 0x6b30ad511d1d2d33, 0xb9f1319657b988dc],
"PokedexEntry": [0xc212013c9be094eb, 0x3fd8f513e363194a, 0x8693cfb8988d3e75, 0x12717c1cc2645816],
"Species": [0xda257fcd856bbf94, 0x1d556c6d7d2f52c5, 0xc46dd65d582a6e48, 0x943b1e876293ae1]
]
)
)
/**
- Important: `addStorageAndWait(_:)` was used here to simplify initializing the demo, but in practice the asynchronous function variants are recommended.
*/
try! dataStack.addStorageAndWait(
SQLiteStore(
fileName: "Modern.PokedexDemo.sqlite",
localStorageOptions: .recreateStoreOnModelMismatch
)
)
return dataStack
}()
static let pokedexEntries: ListPublisher<Modern.PokedexDemo.PokedexEntry> = Modern.PokedexDemo.dataStack.publishList(
From<Modern.PokedexDemo.PokedexEntry>()
.orderBy(.ascending(\.$index))
)
}
}

View File

@@ -1,72 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.Form
/**
Sample 1: This sample shows how to declare `CoreStoreObject` subclasses that implement `ImportableUniqueObject`. For this class the `ImportSource` is a JSON `Dictionary`.
*/
final class Form: CoreStoreObject, ImportableUniqueObject {
// MARK: Internal
@Field.Stored("id")
var id: Int = 0
@Field.Stored("name")
var name: String?
@Field.Stored("spriteURL")
var spriteURL: URL?
@Field.Relationship("details", inverse: \.$forms)
var details: Modern.PokedexDemo.Details?
// MARK: ImportableObject
typealias ImportSource = Dictionary<String, Any>
// MARK: ImportableUniqueObject
typealias UniqueIDType = Int
static let uniqueIDKeyPath: String = String(keyPath: \Modern.PokedexDemo.Form.$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.spriteURL = try? Service.parseJSON(
json["sprites"],
transformer: { (json: Dictionary<String, Any>) in
try Service.parseJSON(json["front_default"], transformer: URL.init(string:))
}
)
}
}
}

View File

@@ -1,74 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.PokedexEntry
/**
Sample 1: This sample shows how to declare `CoreStoreObject` subclasses that implement `ImportableUniqueObject`. For this class the `ImportSource` is a tuple.
*/
final class PokedexEntry: CoreStoreObject, ImportableUniqueObject {
// MARK: Internal
@Field.Stored("index")
var index: Int = 0
@Field.Stored("id")
var id: String = ""
@Field.Stored(
"speciesURL",
dynamicInitialValue: { URL(string: "data:application/json,%7B%7D")! }
)
var speciesURL: URL
@Field.Relationship("details")
var details: Modern.PokedexDemo.Details?
// MARK: ImportableObject
typealias ImportSource = (index: Int, json: Dictionary<String, Any>)
func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws {
self.details = transaction.create(Into<Modern.PokedexDemo.Details>())
try self.update(from: source, in: transaction)
}
// MARK: ImportableUniqueObject
typealias UniqueIDType = String
static let uniqueIDKeyPath: String = String(keyPath: \Modern.PokedexDemo.PokedexEntry.$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.json
return try Modern.PokedexDemo.Service.parseJSON(json["name"])
}
func update(from source: ImportSource, in transaction: BaseDataTransaction) throws {
let json = source.json
self.index = source.index
self.speciesURL = try Modern.PokedexDemo.Service.parseJSON(json["url"], transformer: URL.init(string:))
}
}
}

View File

@@ -1,65 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.Move
/**
Sample 1: Types that will be used with `@Field.Stored` need to implement both `ImportableAttributeType` and `FieldStorableType`. In this case, `RawRepresentable` types with primitive `RawValue`s have built-in implementations so we only have to declare conformance to `ImportableAttributeType` and `FieldStorableType`.
*/
enum PokemonType: String, CaseIterable, ImportableAttributeType, FieldStorableType {
// MARK: Internal
case bug
case dark
case dragon
case electric
case fairy
case fighting
case fire
case flying
case ghost
case grass
case ground
case ice
case normal
case poison
case psychic
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

@@ -1,390 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Foundation
import Combine
import CoreStore
import UIKit
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.Service
final class Service: ObservableObject {
/**
Sample 1: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are tuples
*/
private static func importPokedexEntries(
from output: URLSession.DataTaskPublisher.Output
) -> Future<Void, Modern.PokedexDemo.Service.Error> {
return .init { 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: [])
)
let results: [Dictionary<String, Any>] = try self.parseJSON(
json["results"]
)
_ = 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
switch error {
case .userError(let error as Modern.PokedexDemo.Service.Error):
promise(.failure(error))
case .userError(let error):
promise(.failure(.otherError(error)))
case let error:
promise(.failure(.saveError(error)))
}
}
)
}
}
/**
Sample 2: Importing a single JSON data into an `ImportableUniqueObject` whose `ImportSource` is a JSON `Dictionary`
*/
private static func importSpecies(
for details: ObjectSnapshot<Modern.PokedexDemo.Details>,
from output: URLSession.DataTaskPublisher.Output
) -> Future<ObjectSnapshot<Modern.PokedexDemo.Species>, Modern.PokedexDemo.Service.Error> {
return .init { promise in
Modern.PokedexDemo.dataStack.perform(
asynchronous: { transaction -> Modern.PokedexDemo.Species in
let json: Dictionary<String, Any> = try self.parseJSON(
try JSONSerialization.jsonObject(with: output.data, options: [])
)
guard
let species = try transaction.importUniqueObject(
Into<Modern.PokedexDemo.Species>(),
source: json
)
else {
throw Modern.PokedexDemo.Service.Error.unexpected
}
details.asEditable(in: transaction)?.species = species
return species
},
success: { species in
promise(.success(species.asSnapshot(in: Modern.PokedexDemo.dataStack)!))
},
failure: { error in
switch error {
case .userError(let error as Modern.PokedexDemo.Service.Error):
promise(.failure(error))
case .userError(let error):
promise(.failure(.otherError(error)))
case let error:
promise(.failure(.saveError(error)))
}
}
)
}
}
/**
Sample 3: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are JSON `Dictionary`s
*/
private static func importForms(
for details: ObjectSnapshot<Modern.PokedexDemo.Details>,
from outputs: [URLSession.DataTaskPublisher.Output]
) -> Future<Void, Modern.PokedexDemo.Service.Error> {
return .init { promise in
Modern.PokedexDemo.dataStack.perform(
asynchronous: { transaction -> Void in
let forms = try transaction.importUniqueObjects(
Into<Modern.PokedexDemo.Form>(),
sourceArray: outputs.map { output in
return try self.parseJSON(
try JSONSerialization.jsonObject(with: output.data, options: [])
)
}
)
guard !forms.isEmpty else {
throw Modern.PokedexDemo.Service.Error.unexpected
}
details.asEditable(in: transaction)?.forms = forms
},
success: {
promise(.success(()))
},
failure: { error in
switch error {
case .userError(let error as Modern.PokedexDemo.Service.Error):
promise(.failure(error))
case .userError(let error):
promise(.failure(.otherError(error)))
case let error:
promise(.failure(.saveError(error)))
}
}
)
}
}
// MARK: Internal
private(set) var isLoading: Bool = false {
willSet {
self.objectWillChange.send()
}
}
private(set) var lastError: (error: Modern.PokedexDemo.Service.Error, retry: () -> Void)? {
willSet {
self.objectWillChange.send()
}
}
init() {}
static func parseJSON<Output>(
_ json: Any?,
file: StaticString = #file,
line: Int = #line
) throws -> Output {
switch json {
case let json as Output:
return json
case let any:
throw Modern.PokedexDemo.Service.Error.parseError(
expected: Output.self,
actual: type(of: any),
file: "\(file):\(line)"
)
}
}
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 = try transformer(json)
if let json = transformed {
return json
}
throw Modern.PokedexDemo.Service.Error.parseError(
expected: Output.self,
actual: type(of: transformed),
file: "\(file):\(line)"
)
case let any:
throw Modern.PokedexDemo.Service.Error.parseError(
expected: Output.self,
actual: type(of: any),
file: "\(file):\(line)"
)
}
}
func fetchPokedexEntries() {
self.cancellable["pokedexEntries"] = self.pokedexEntries
.receive(on: DispatchQueue.main)
.handleEvents(
receiveSubscription: { [weak self] _ in
guard let self = self else {
return
}
self.lastError = nil
self.isLoading = true
}
)
.sink(
receiveCompletion: { [weak self] completion in
guard let self = self else {
return
}
self.isLoading = false
switch completion {
case .finished:
self.lastError = nil
case .failure(let error):
print(error)
self.lastError = (
error: error,
retry: { [weak self] in
self?.fetchPokedexEntries()
}
)
}
},
receiveValue: {}
)
}
func fetchDetails(for pokedexEntry: ObjectSnapshot<Modern.PokedexDemo.PokedexEntry>) {
self.fetchSpeciesIfNeeded(for: pokedexEntry)
}
// MARK: Private
private var cancellable: Dictionary<String, AnyCancellable> = [:]
private lazy var pokedexEntries: AnyPublisher<Void, Modern.PokedexDemo.Service.Error> = URLSession.shared
.dataTaskPublisher(
for: URL(string: "https://pokeapi.co/api/v2/pokemon?limit=10000&offset=0")!
)
.mapError({ .networkError($0) })
.flatMap(Self.importPokedexEntries(from:))
.eraseToAnyPublisher()
private func fetchSpeciesIfNeeded(for pokedexEntry: ObjectSnapshot<Modern.PokedexDemo.PokedexEntry>) {
guard let details = pokedexEntry.$details?.snapshot else {
return
}
if let species = details.$species?.snapshot {
self.fetchFormsIfNeeded(for: species)
return
}
self.cancellable["species.\(pokedexEntry.$id)"] = URLSession.shared
.dataTaskPublisher(for: pokedexEntry.$speciesURL)
.mapError({ .networkError($0) })
.flatMap({ Self.importSpecies(for: details, from: $0) })
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
},
receiveValue: { species in
self.fetchFormsIfNeeded(for: species)
}
)
}
private func fetchFormsIfNeeded(for species: ObjectSnapshot<Modern.PokedexDemo.Species>) {
guard
let details = species.$details?.snapshot,
details.$forms.isEmpty
else {
return
}
self.cancellable["forms.\(species.$id)"] = species
.$formsURLs
.map(
{
URLSession.shared
.dataTaskPublisher(for: $0)
.mapError({ Modern.PokedexDemo.Service.Error.networkError($0) })
.eraseToAnyPublisher()
}
)
.reduce(
into: Just<[URLSession.DataTaskPublisher.Output]>([])
.setFailureType(to: Modern.PokedexDemo.Service.Error.self)
.eraseToAnyPublisher(),
{ (result, publisher) in
result = result
.zip(publisher, { $0 + [$1] })
.eraseToAnyPublisher()
}
)
.flatMap({ Self.importForms(for: details, from: $0) })
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
},
receiveValue: { _ in }
)
}
// MARK: - Modern.PokedexDemo.Service.Error
enum Error: Swift.Error {
case networkError(URLError)
case parseError(expected: Any.Type, actual: Any.Type, file: String)
case saveError(CoreStoreError)
case otherError(Swift.Error)
case unexpected
}
}
}

View File

@@ -1,139 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.Species
/**
Sample 1: This sample shows how to declare `CoreStoreObject` subclasses that implement `ImportableUniqueObject`. For this class the `ImportSource` is a JSON `Dictionary`.
*/
final class Species: CoreStoreObject, ImportableUniqueObject {
// MARK: Internal
@Field.Stored("id")
var id: Int = 0
@Field.Stored("name")
var name: String = ""
@Field.Stored("weight")
var weight: Int = 0
@Field.Stored("pokemonType1")
var pokemonType1: Modern.PokedexDemo.PokemonType = .normal
@Field.Stored("pokemonType2")
var pokemonType2: Modern.PokedexDemo.PokemonType?
@Field.Stored("statHitPoints")
var statHitPoints: Int = 0
@Field.Stored("statAttack")
var statAttack: Int = 0
@Field.Stored("statDefense")
var statDefense: Int = 0
@Field.Stored("statSpecialAttack")
var statSpecialAttack: Int = 0
@Field.Stored("statSpecialDefense")
var statSpecialDefense: Int = 0
@Field.Stored("statSpeed")
var statSpeed: Int = 0
@Field.Coded(
"formsURLs",
coder: FieldCoders.Json.self
)
var formsURLs: [URL] = []
@Field.Relationship("details", inverse: \.$species)
var details: Modern.PokedexDemo.Details?
// MARK: ImportableObject
typealias ImportSource = Dictionary<String, Any>
// MARK: ImportableUniqueObject
typealias UniqueIDType = Int
static let uniqueIDKeyPath: String = String(keyPath: \Modern.PokedexDemo.Species.$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
}
}
self.formsURLs = try (Service.parseJSON(json["forms"]) as [Dictionary<String, Any>])
.map({ try Service.parseJSON($0["url"], transformer: URL.init(string:)) })
}
}
}