mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-02-26 01:25:01 +01:00
improve Pokedex demo
This commit is contained in:
@@ -11,8 +11,11 @@
|
||||
B531EFE924EB5A53005F247D /* Modern.PokedexDemo.PokedexEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B531EFE824EB5A52005F247D /* Modern.PokedexDemo.PokedexEntry.swift */; };
|
||||
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 */; };
|
||||
B566C8E824F9D406001134A1 /* Modern.PokedexDemo.ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B566C8E724F9D406001134A1 /* Modern.PokedexDemo.ListView.swift */; };
|
||||
B566C8EA24F9D412001134A1 /* Modern.PokedexDemo.ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B566C8E924F9D412001134A1 /* Modern.PokedexDemo.ListViewController.swift */; };
|
||||
B566C8EC24F9D694001134A1 /* Modern.PokedexDemo.ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B566C8EB24F9D694001134A1 /* Modern.PokedexDemo.ItemCell.swift */; };
|
||||
B566C8EE24FA1EA3001134A1 /* Modern.PokedexDemo.PokemonDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = B566C8ED24FA1EA3001134A1 /* Modern.PokedexDemo.PokemonDetails.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 */; };
|
||||
@@ -63,10 +66,9 @@
|
||||
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.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 */; };
|
||||
B5A391BD24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391BC24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift */; };
|
||||
B5E32C9024FA41F9003F46AD /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E32C8F24FA41F9003F46AD /* ImageDownloader.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -91,8 +93,11 @@
|
||||
B531EFE824EB5A52005F247D /* Modern.PokedexDemo.PokedexEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokedexEntry.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
B566C8E724F9D406001134A1 /* Modern.PokedexDemo.ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.ListView.swift; sourceTree = "<group>"; };
|
||||
B566C8E924F9D412001134A1 /* Modern.PokedexDemo.ListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.ListViewController.swift; sourceTree = "<group>"; };
|
||||
B566C8EB24F9D694001134A1 /* Modern.PokedexDemo.ItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.ItemCell.swift; sourceTree = "<group>"; };
|
||||
B566C8ED24FA1EA3001134A1 /* Modern.PokedexDemo.PokemonDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokemonDetails.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>"; };
|
||||
@@ -142,10 +147,9 @@
|
||||
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.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>"; };
|
||||
B5A391BC24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.Ability.swift; sourceTree = "<group>"; };
|
||||
B5E32C8F24FA41F9003F46AD /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -303,6 +307,7 @@
|
||||
B5A3917B24E6A76C00E7E8BD /* LazyView.swift */,
|
||||
B5A3917F24E787D900E7E8BD /* InstructionsView.swift */,
|
||||
B566C8E524ED6B98001134A1 /* NetworkImageView.swift */,
|
||||
B5E32C8F24FA41F9003F46AD /* ImageDownloader.swift */,
|
||||
B5A391A724E90F1000E7E8BD /* UIImage+Extensions.swift */,
|
||||
B5A3915424E6857F00E7E8BD /* Menu */,
|
||||
);
|
||||
@@ -380,7 +385,9 @@
|
||||
B5A391B024E96AF600E7E8BD /* Modern.PokedexDemo.swift */,
|
||||
B531EFEA24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift */,
|
||||
B531EFEC24EB7453005F247D /* Modern.PokedexDemo.MainView.swift */,
|
||||
B54269C524ED190C00A66C23 /* Modern.PokedexDemo.ItemView.swift */,
|
||||
B566C8E724F9D406001134A1 /* Modern.PokedexDemo.ListView.swift */,
|
||||
B566C8E924F9D412001134A1 /* Modern.PokedexDemo.ListViewController.swift */,
|
||||
B566C8EB24F9D694001134A1 /* Modern.PokedexDemo.ItemCell.swift */,
|
||||
B5A391B224E96B7400E7E8BD /* Models */,
|
||||
);
|
||||
path = PokedexDemo;
|
||||
@@ -390,10 +397,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B531EFE824EB5A52005F247D /* Modern.PokedexDemo.PokedexEntry.swift */,
|
||||
B566C8ED24FA1EA3001134A1 /* Modern.PokedexDemo.PokemonDetails.swift */,
|
||||
B5A391B824E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift */,
|
||||
B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonDisplay.swift */,
|
||||
B5A391B524E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift */,
|
||||
B5A391BC24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift */,
|
||||
B5A391B724E96E8600E7E8BD /* Attributes */,
|
||||
);
|
||||
name = Models;
|
||||
@@ -483,8 +489,10 @@
|
||||
B5A3915924E685EC00E7E8BD /* Classic.swift in Sources */,
|
||||
B5A3918024E787D900E7E8BD /* InstructionsView.swift in Sources */,
|
||||
B5A3917C24E6A76C00E7E8BD /* LazyView.swift in Sources */,
|
||||
B566C8EC24F9D694001134A1 /* Modern.PokedexDemo.ItemCell.swift in Sources */,
|
||||
B5A3913424E6170500E7E8BD /* Menu.swift in Sources */,
|
||||
B5A3915B24E685FE00E7E8BD /* Modern.swift in Sources */,
|
||||
B566C8EA24F9D412001134A1 /* Modern.PokedexDemo.ListViewController.swift in Sources */,
|
||||
B5A3911F24E5429200E7E8BD /* SceneDelegate.swift in Sources */,
|
||||
B5A3915324E6537F00E7E8BD /* Menu.ItemView.swift in Sources */,
|
||||
B5A3912124E5429200E7E8BD /* Menu.MainView.swift in Sources */,
|
||||
@@ -504,18 +512,18 @@
|
||||
B5A3916224E697BA00E7E8BD /* Modern.PlacemarksDemo.MainView.swift in Sources */,
|
||||
B5A3916024E6925900E7E8BD /* Modern.PlacemarksDemo.MapView.swift in Sources */,
|
||||
B5A3916524E698C700E7E8BD /* Modern.PlacemarksDemo.Place.swift in Sources */,
|
||||
B5A391BD24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift in Sources */,
|
||||
B5A391B624E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift in Sources */,
|
||||
B54269C624ED190C00A66C23 /* Modern.PokedexDemo.ItemView.swift in Sources */,
|
||||
B531EFE924EB5A53005F247D /* Modern.PokedexDemo.PokedexEntry.swift in Sources */,
|
||||
B5A391B924E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift in Sources */,
|
||||
B566C8E824F9D406001134A1 /* Modern.PokedexDemo.ListView.swift in Sources */,
|
||||
B5A391B424E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonDisplay.swift in Sources */,
|
||||
B5A391BB24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift in Sources */,
|
||||
B5A3918C24E7B44B00E7E8BD /* Modern.TimeZonesDemo.ItemView.swift in Sources */,
|
||||
B5E32C9024FA41F9003F46AD /* ImageDownloader.swift in Sources */,
|
||||
B5A3918A24E7AD1800E7E8BD /* Modern.TimeZonesDemo.ListView.swift in Sources */,
|
||||
B5A3918824E7A8F900E7E8BD /* Modern.TimeZonesDemo.MainView.swift in Sources */,
|
||||
B531EFED24EB7453005F247D /* Modern.PokedexDemo.MainView.swift in Sources */,
|
||||
B5A3918624E7A54A00E7E8BD /* Modern.TimeZonesDemo.TimeZone.swift in Sources */,
|
||||
B566C8EE24FA1EA3001134A1 /* Modern.PokedexDemo.PokemonDetails.swift in Sources */,
|
||||
B5A3919A24E8207A00E7E8BD /* Modern.ColorsDemo.SwiftUI.DetailView.swift in Sources */,
|
||||
B5A3919624E7E4AC00E7E8BD /* Modern.ColorsDemo.SwiftUI.ItemView.swift in Sources */,
|
||||
B5A3919424E7E36700E7E8BD /* Modern.ColorsDemo.SwiftUI.ListView.swift in Sources */,
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
extension Modern.PokedexDemo {
|
||||
|
||||
// MARK: - Modern.PokedexDemo.Ability
|
||||
|
||||
final class Ability: CoreStoreObject {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
@Field.Stored("id")
|
||||
var id: Int = 0
|
||||
|
||||
@Field.Stored("name")
|
||||
var name: String = ""
|
||||
|
||||
@Field.Stored("text")
|
||||
var text: String = ""
|
||||
|
||||
@Field.Stored("isHiddenAbility")
|
||||
var isHiddenAbility: Bool = false
|
||||
|
||||
|
||||
@Field.Relationship("learners")
|
||||
var learners: Set<Modern.PokedexDemo.PokemonForm>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
//
|
||||
// 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.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.pokemonDetails = newValue.snapshot?.$pokemonDetails
|
||||
|
||||
self.didUpdateData(animated: true)
|
||||
}
|
||||
|
||||
self.pokemonDetails = newValue?.snapshot?.$pokemonDetails
|
||||
}
|
||||
}
|
||||
|
||||
private var pokemonDetails: ObjectPublisher<Modern.PokedexDemo.PokemonDetails>? {
|
||||
|
||||
didSet {
|
||||
|
||||
let newValue = self.pokemonDetails
|
||||
guard newValue != oldValue else {
|
||||
|
||||
return
|
||||
}
|
||||
oldValue?.removeObserver(self)
|
||||
newValue?.addObserver(self) { [weak self] newValue in
|
||||
|
||||
guard let self = self else {
|
||||
|
||||
return
|
||||
}
|
||||
let pokemonDetails = newValue.snapshot
|
||||
self.pokemonForm = pokemonDetails?.$pokemonForm
|
||||
self.pokemonDisplays = pokemonDetails?.$pokemonDisplays
|
||||
|
||||
self.didUpdateData(animated: true)
|
||||
}
|
||||
|
||||
let pokemonDetails = newValue?.snapshot
|
||||
self.pokemonForm = pokemonDetails?.$pokemonForm
|
||||
self.pokemonDisplays = pokemonDetails?.$pokemonDisplays
|
||||
}
|
||||
}
|
||||
|
||||
private var pokemonForm: ObjectPublisher<Modern.PokedexDemo.PokemonForm>? {
|
||||
|
||||
didSet {
|
||||
|
||||
let newValue = self.pokemonForm
|
||||
guard newValue != oldValue else {
|
||||
|
||||
return
|
||||
}
|
||||
oldValue?.removeObserver(self)
|
||||
newValue?.addObserver(self) { [weak self] _ in
|
||||
|
||||
self?.didUpdateData(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var rotationCancellable: AnyCancellable?
|
||||
private var pokemonDisplays: [ObjectPublisher<Modern.PokedexDemo.PokemonDisplay>]? {
|
||||
|
||||
didSet {
|
||||
|
||||
let newValue = self.pokemonDisplays
|
||||
guard newValue != oldValue else {
|
||||
|
||||
return
|
||||
}
|
||||
self.pokemonDisplay = newValue?.first
|
||||
|
||||
self.rotationCancellable = 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.pokemonDisplay = newValue[index % newValue.count]
|
||||
self.didUpdateData(animated: true)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var pokemonDisplay: ObjectPublisher<Modern.PokedexDemo.PokemonDisplay>? {
|
||||
|
||||
didSet {
|
||||
|
||||
let newValue = self.pokemonDisplay
|
||||
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 pokemonForm = self.pokemonForm?.snapshot
|
||||
let pokemonDisplay = self.pokemonDisplay?.snapshot
|
||||
|
||||
self.placeholderLabel.text = pokedexEntry?.$id
|
||||
self.placeholderLabel.isHidden = pokemonForm != nil
|
||||
|
||||
self.type1View.backgroundColor = pokemonForm?.$pokemonType1.color
|
||||
?? UIColor.clear
|
||||
self.type1View.isHidden = pokemonForm == nil
|
||||
|
||||
self.type2View.backgroundColor = pokemonForm?.$pokemonType2?.color
|
||||
?? pokemonForm?.$pokemonType1.color
|
||||
?? UIColor.clear
|
||||
self.type2View.isHidden = pokemonForm == nil
|
||||
|
||||
self.nameLabel.text = pokemonDisplay?.$name ?? pokemonForm?.$name
|
||||
self.nameLabel.isHidden = pokemonDisplay == nil && pokemonForm == nil
|
||||
|
||||
self.imageURL = pokemonDisplay?.$spriteURL
|
||||
|
||||
guard animated else {
|
||||
|
||||
return
|
||||
}
|
||||
self.contentView.layer.add(CATransition(), forKey: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,141 +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.ItemView
|
||||
|
||||
struct ItemView: View {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static let preferredHeight: CGFloat = 100
|
||||
|
||||
init(
|
||||
pokedexEntry: ObjectPublisher<Modern.PokedexDemo.PokedexEntry>,
|
||||
service: Modern.PokedexDemo.Service
|
||||
) {
|
||||
|
||||
self.pokedexEntry = pokedexEntry
|
||||
self.service = service
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
|
||||
let pokedexEntry = self.pokedexEntry.snapshot
|
||||
let pokemonForm = pokedexEntry?.$pokemonForm?.snapshot
|
||||
let pokemonDisplay = pokemonForm?.$pokemonDisplay?.snapshot
|
||||
|
||||
return HStack(spacing: 10) {
|
||||
|
||||
LazyView {
|
||||
|
||||
NetworkImageView(url: pokemonDisplay?.$spriteURL)
|
||||
.frame(width: 70, height: 70)
|
||||
.id(pokemonDisplay)
|
||||
}
|
||||
ZStack {
|
||||
{ () -> AnyView in
|
||||
if let pokemonForm = pokemonForm {
|
||||
|
||||
return AnyView(
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
HStack {
|
||||
Text(pokemonDisplay?.$displayName ?? pokemonForm.$name)
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
self.view(for: pokemonForm.$pokemonType1)
|
||||
pokemonForm.$pokemonType2.map(self.view(for:))
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
)
|
||||
}
|
||||
else {
|
||||
|
||||
return AnyView(
|
||||
Text(pokedexEntry?.$id ?? "")
|
||||
.foregroundColor(Color(UIColor.placeholderText))
|
||||
.fontWeight(.heavy)
|
||||
.frame(maxWidth: .infinity)
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding()
|
||||
.onAppear {
|
||||
|
||||
if let pokedexEntry = pokedexEntry {
|
||||
|
||||
self.service.fetchPokemonForm(for: pokedexEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@ObservedObject
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_PokedexDemo_ItemView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static let service = Modern.PokedexDemo.Service()
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
try! Modern.PokedexDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
guard (try transaction.fetchCount(From<Modern.PokedexDemo.PokedexEntry>())) <= 0 else {
|
||||
return
|
||||
}
|
||||
let pokedexEntry = transaction.create(Into<Modern.PokedexDemo.PokedexEntry>())
|
||||
pokedexEntry.id = "bulbasaur"
|
||||
pokedexEntry.pokemonFormURL = URL(string: "https://pokeapi.co/api/v2/pokemon/1/")!
|
||||
}
|
||||
)
|
||||
|
||||
return Modern.PokedexDemo.ItemView(
|
||||
pokedexEntry: Modern.PokedexDemo.pokedexEntries.snapshot.first!,
|
||||
service: Modern.PokedexDemo.Service()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// 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
|
||||
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// 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 {
|
||||
|
||||
/**
|
||||
⭐️ Sample 1: Setting up a `DiffableDataSource.TableViewAdapter` that will manage tableView snapshot updates automatically. We can use the built-in `DiffableDataSource.TableViewAdapter` type directly, but in our case we want to enabled `UITableView` cell deletions so we create a custom subclass `DeletionEnabledDataSource` (see declatation below).
|
||||
*/
|
||||
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
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
⭐️ Sample 2: Once the views are created, we can start binding `ListPublisher` updates to the `DiffableDataSource`. We typically call this at the end of `viewDidLoad`. Note that the `addObserver`'s closure argument will only be called on the succeeding updates, so to immediately display the current values, we need to call `dataSource.apply()` once.
|
||||
*/
|
||||
private func startObservingList() {
|
||||
|
||||
self.listPublisher.addObserver(self) { (listPublisher) in
|
||||
|
||||
self.dataSource.apply(
|
||||
listPublisher.snapshot,
|
||||
animatingDifferences: true
|
||||
)
|
||||
}
|
||||
self.dataSource.apply(
|
||||
self.listPublisher.snapshot,
|
||||
animatingDifferences: false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
⭐️ Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ListPublisher`s safely remove deallocated observers automatically.
|
||||
*/
|
||||
deinit {
|
||||
|
||||
self.listPublisher.removeObserver(self)
|
||||
}
|
||||
|
||||
|
||||
// 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 = floor((screenWidth - ((cellsPerRow - 1) * layout.minimumInteritemSpacing)) / cellsPerRow)
|
||||
layout.itemSize = .init(
|
||||
width: cellWidth,
|
||||
height: ceil(cellWidth * (4 / 3))
|
||||
)
|
||||
super.init(collectionViewLayout: layout)
|
||||
}
|
||||
|
||||
|
||||
// MARK: UIViewController
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
super.viewDidLoad()
|
||||
|
||||
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>
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,15 +33,21 @@ extension Modern.PokedexDemo {
|
||||
|
||||
var body: some View {
|
||||
let pokedexEntries = self.pokedexEntries.snapshot
|
||||
let visibleItems = self.visibleItems
|
||||
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: 20) {
|
||||
Text("This demo needs to make a network connection to download Pokedex entries")
|
||||
if self.service.isLoading {
|
||||
|
||||
|
||||
Text("Fetching Pokedex…")
|
||||
}
|
||||
else {
|
||||
@@ -49,7 +55,7 @@ extension Modern.PokedexDemo {
|
||||
Button(
|
||||
action: { self.service.fetchPokedexEntries() },
|
||||
label: {
|
||||
|
||||
|
||||
Text("Download Pokedex Entries")
|
||||
}
|
||||
)
|
||||
@@ -57,33 +63,6 @@ extension Modern.PokedexDemo {
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
else {
|
||||
|
||||
List {
|
||||
|
||||
ForEach(0 ..< min(visibleItems, pokedexEntries.count), id: \.self) { index in
|
||||
LazyView {
|
||||
Modern.PokedexDemo.ItemView(
|
||||
pokedexEntry: pokedexEntries[index],
|
||||
service: self.service
|
||||
)
|
||||
}
|
||||
.frame(height: Modern.PokedexDemo.ItemView.preferredHeight)
|
||||
.frame(minWidth: 0, maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
if visibleItems < pokedexEntries.count {
|
||||
|
||||
Spacer(minLength: Modern.PokedexDemo.ItemView.preferredHeight)
|
||||
.onAppear {
|
||||
|
||||
self.visibleItems = min(
|
||||
visibleItems + 50,
|
||||
pokedexEntries.count
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Pokedex")
|
||||
}
|
||||
@@ -93,9 +72,6 @@ extension Modern.PokedexDemo {
|
||||
|
||||
@ObservedObject
|
||||
private var service: Modern.PokedexDemo.Service = .init()
|
||||
|
||||
@State
|
||||
private var visibleItems: Int = 50
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
extension Modern.PokedexDemo {
|
||||
|
||||
// MARK: - Modern.PokedexDemo.Move
|
||||
|
||||
final class Move: CoreStoreObject {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
@Field.Stored("id")
|
||||
var id: Int = 0
|
||||
|
||||
@Field.Stored("name")
|
||||
var name: String = ""
|
||||
|
||||
@Field.Stored("text")
|
||||
var text: String = ""
|
||||
|
||||
@Field.Stored("pokemonType")
|
||||
var pokemonType: Modern.PokedexDemo.PokemonType = .normal
|
||||
|
||||
@Field.Stored("power")
|
||||
var power: Int = 0
|
||||
|
||||
@Field.Stored("accuracy")
|
||||
var accuracy: Int = 0
|
||||
|
||||
@Field.Stored("powerPoints")
|
||||
var powerPoints: Int = 0
|
||||
|
||||
@Field.Stored("effectChance")
|
||||
var effectChance: Int = 0
|
||||
|
||||
@Field.Stored("priority")
|
||||
var priority: Int = 0
|
||||
|
||||
|
||||
@Field.Relationship("learners")
|
||||
var learners: Set<Modern.PokedexDemo.PokemonForm>
|
||||
}
|
||||
}
|
||||
@@ -25,21 +25,21 @@ extension Modern.PokedexDemo {
|
||||
dynamicInitialValue: { URL(string: "data:application/json,%7B%7D")! }
|
||||
)
|
||||
var pokemonFormURL: URL
|
||||
|
||||
@Field.Stored(
|
||||
"updateHash",
|
||||
dynamicInitialValue: { UUID() }
|
||||
)
|
||||
var updateHash: UUID
|
||||
|
||||
|
||||
@Field.Relationship("pokemonForm")
|
||||
var pokemonForm: Modern.PokedexDemo.PokemonForm?
|
||||
@Field.Relationship("pokemonDetails")
|
||||
var pokemonDetails: Modern.PokedexDemo.PokemonDetails?
|
||||
|
||||
|
||||
// MARK: ImportableObject
|
||||
|
||||
typealias ImportSource = (index: Int, json: Dictionary<String, Any>)
|
||||
|
||||
func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws {
|
||||
|
||||
self.pokemonDetails = transaction.create(Into<Modern.PokedexDemo.PokemonDetails>())
|
||||
try self.update(from: source, in: transaction)
|
||||
}
|
||||
|
||||
|
||||
// MARK: ImportableUniqueObject
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
import UIKit
|
||||
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
extension Modern.PokedexDemo {
|
||||
|
||||
// MARK: - Modern.PokedexDemo.PokemonDetails
|
||||
|
||||
final class PokemonDetails: CoreStoreObject {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
@Field.Relationship("pokedexEntry", inverse: \.$pokemonDetails)
|
||||
var pokedexEntry: Modern.PokedexDemo.PokedexEntry?
|
||||
|
||||
@Field.Relationship("pokemonForm")
|
||||
var pokemonForm: Modern.PokedexDemo.PokemonForm?
|
||||
|
||||
@Field.Relationship("pokemonDisplays")
|
||||
var pokemonDisplays: [Modern.PokedexDemo.PokemonDisplay]
|
||||
}
|
||||
}
|
||||
@@ -18,15 +18,15 @@ extension Modern.PokedexDemo {
|
||||
@Field.Stored("id")
|
||||
var id: Int = 0
|
||||
|
||||
@Field.Stored("displayName")
|
||||
var displayName: String?
|
||||
@Field.Stored("name")
|
||||
var name: String?
|
||||
|
||||
@Field.Stored("spriteURL")
|
||||
var spriteURL: URL?
|
||||
|
||||
|
||||
@Field.Relationship("form")
|
||||
var pokedexForm: Modern.PokedexDemo.PokemonForm?
|
||||
|
||||
@Field.Relationship("pokemonDetails", inverse: \.$pokemonDisplays)
|
||||
var pokemonDetails: Modern.PokedexDemo.PokemonDetails?
|
||||
|
||||
|
||||
// MARK: ImportableObject
|
||||
@@ -57,22 +57,8 @@ extension Modern.PokedexDemo {
|
||||
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(
|
||||
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:))
|
||||
|
||||
@@ -49,25 +49,15 @@ extension Modern.PokedexDemo {
|
||||
var statSpeed: Int = 0
|
||||
|
||||
|
||||
@Field.Stored(
|
||||
"pokemonDisplayURL",
|
||||
dynamicInitialValue: { URL(string: "data:application/json,%7B%7D")! }
|
||||
@Field.Coded(
|
||||
"pokemonDisplayURLs",
|
||||
coder: FieldCoders.Json.self
|
||||
)
|
||||
var pokemonDisplayURL: URL
|
||||
|
||||
|
||||
@Field.Relationship("display", inverse: \.$pokedexForm)
|
||||
var pokemonDisplay: Modern.PokedexDemo.PokemonDisplay?
|
||||
|
||||
@Field.Relationship("abilities", inverse: \.$learners)
|
||||
var abilities: Set<Modern.PokedexDemo.Ability>
|
||||
|
||||
@Field.Relationship("moves", inverse: \.$learners)
|
||||
var moves: Set<Modern.PokedexDemo.Move>
|
||||
var pokemonDisplayURLs: [URL] = []
|
||||
|
||||
|
||||
@Field.Relationship("pokedexEntry", inverse: \.$pokemonForm)
|
||||
var pokedexEntry: Modern.PokedexDemo.PokedexEntry?
|
||||
@Field.Relationship("pokemonDetails", inverse: \.$pokemonForm)
|
||||
var pokemonDetails: Modern.PokedexDemo.PokemonDetails?
|
||||
|
||||
|
||||
// MARK: ImportableObject
|
||||
@@ -139,25 +129,10 @@ extension Modern.PokedexDemo {
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
self.pokemonDisplayURLs = try (Service.parseJSON(json["forms"]) as [Dictionary<String, Any>]).map { json in
|
||||
|
||||
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
|
||||
return pokemonDisplayURL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,12 +128,21 @@ extension Modern.PokedexDemo {
|
||||
receiveValue: {}
|
||||
)
|
||||
}
|
||||
|
||||
func fetchDetails(for pokedexEntry: ObjectSnapshot<Modern.PokedexDemo.PokedexEntry>) {
|
||||
|
||||
self.fetchPokemonFormIfNeeded(for: pokedexEntry)
|
||||
}
|
||||
|
||||
func fetchPokemonForm(for pokedexEntry: ObjectSnapshot<Modern.PokedexDemo.PokedexEntry>) {
|
||||
|
||||
if let pokedexForm = pokedexEntry.$pokemonForm?.snapshot {
|
||||
private func fetchPokemonFormIfNeeded(for pokedexEntry: ObjectSnapshot<Modern.PokedexDemo.PokedexEntry>) {
|
||||
|
||||
guard let pokemonDetails = pokedexEntry.$pokemonDetails?.snapshot else {
|
||||
|
||||
self.fetchPokemonDisplay(for: pokedexForm)
|
||||
return
|
||||
}
|
||||
if let pokedexForm = pokemonDetails.$pokemonForm?.snapshot {
|
||||
|
||||
self.fetchPokemonDisplayIfNeeded(for: pokedexForm)
|
||||
return
|
||||
}
|
||||
self.cancellable["pokemonForm.\(pokedexEntry.$id)"] = URLSession.shared
|
||||
@@ -157,11 +166,7 @@ extension Modern.PokedexDemo {
|
||||
|
||||
throw Modern.PokedexDemo.Service.Error.unexpected
|
||||
}
|
||||
if let pokedexEntry = pokedexEntry.asEditable(in: transaction) {
|
||||
|
||||
pokedexForm.pokedexEntry = pokedexEntry
|
||||
pokedexEntry.updateHash = .init()
|
||||
}
|
||||
pokemonDetails.asEditable(in: transaction)?.pokemonForm = pokedexForm
|
||||
return pokedexForm
|
||||
},
|
||||
success: { pokemonForm in
|
||||
@@ -204,43 +209,80 @@ extension Modern.PokedexDemo {
|
||||
},
|
||||
receiveValue: { pokemonForm in
|
||||
|
||||
self.fetchPokemonDisplay(for: pokemonForm)
|
||||
self.fetchPokemonDisplayIfNeeded(for: pokemonForm)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func fetchPokemonDisplay(for pokemonForm: ObjectSnapshot<Modern.PokedexDemo.PokemonForm>) {
|
||||
|
||||
if let pokemonDisplay = pokemonForm.$pokemonDisplay?.snapshot {
|
||||
func fetchPokemonDisplayIfNeeded(for pokemonForm: ObjectSnapshot<Modern.PokedexDemo.PokemonForm>) {
|
||||
|
||||
guard
|
||||
let pokemonDetails = pokemonForm.$pokemonDetails?.snapshot,
|
||||
pokemonDetails.$pokemonDisplays.isEmpty
|
||||
else {
|
||||
|
||||
return
|
||||
}
|
||||
self.cancellable["pokemonDisplay.\(pokemonForm.$id)"] = URLSession.shared
|
||||
.dataTaskPublisher(for: pokemonForm.$pokemonDisplayURL)
|
||||
.mapError({ .networkError($0) })
|
||||
|
||||
|
||||
|
||||
self.cancellable["pokemonDisplay.\(pokemonForm.$id)"] = pokemonForm
|
||||
.$pokemonDisplayURLs
|
||||
.map(
|
||||
{ url in
|
||||
URLSession.shared
|
||||
.dataTaskPublisher(for: url)
|
||||
.mapError({ Modern.PokedexDemo.Service.Error.networkError($0) })
|
||||
.tryMap(
|
||||
{ output -> Dictionary<String, Any> in
|
||||
|
||||
try Self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: output.data, options: [])
|
||||
)
|
||||
}
|
||||
)
|
||||
.mapError(
|
||||
{ error -> Modern.PokedexDemo.Service.Error in
|
||||
switch error {
|
||||
|
||||
case let error as Modern.PokedexDemo.Service.Error:
|
||||
return error
|
||||
|
||||
case let error:
|
||||
return Modern.PokedexDemo.Service.Error.otherError(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
)
|
||||
.reduce(
|
||||
into: Just<[Dictionary<String, Any>]>([])
|
||||
.setFailureType(to: Modern.PokedexDemo.Service.Error.self)
|
||||
.eraseToAnyPublisher(),
|
||||
{ (result, publisher) in
|
||||
result = result
|
||||
.zip(publisher, { $0 + [$1] })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
)
|
||||
.flatMap(
|
||||
{ output in
|
||||
{ outputs 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(
|
||||
let pokemonDisplays = try transaction.importUniqueObjects(
|
||||
Into<Modern.PokedexDemo.PokemonDisplay>(),
|
||||
source: json
|
||||
) else {
|
||||
sourceArray: outputs
|
||||
)
|
||||
guard !pokemonDisplays.isEmpty else {
|
||||
|
||||
throw Modern.PokedexDemo.Service.Error.unexpected
|
||||
}
|
||||
if let pokemonForm = pokemonForm.asEditable(in: transaction) {
|
||||
|
||||
pokemonDisplay.pokedexForm = pokemonForm
|
||||
pokemonForm.pokedexEntry?.updateHash = .init()
|
||||
}
|
||||
pokemonDetails.asEditable(in: transaction)?.pokemonDisplays = pokemonDisplays
|
||||
},
|
||||
success: {
|
||||
|
||||
|
||||
@@ -24,10 +24,9 @@ extension Modern {
|
||||
modelVersion: "V1",
|
||||
entities: [
|
||||
Entity<Modern.PokedexDemo.PokedexEntry>("PokedexEntry"),
|
||||
Entity<Modern.PokedexDemo.PokemonDetails>("PokemonDetails"),
|
||||
Entity<Modern.PokedexDemo.PokemonForm>("PokemonForm"),
|
||||
Entity<Modern.PokedexDemo.PokemonDisplay>("PokemonDisplay"),
|
||||
Entity<Modern.PokedexDemo.Move>("Move"),
|
||||
Entity<Modern.PokedexDemo.Ability>("Ability")
|
||||
Entity<Modern.PokedexDemo.PokemonDisplay>("PokemonDisplay")
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
72
Demo/Sources/Helpers/ImageDownloader.swift
Normal file
72
Demo/Sources/Helpers/ImageDownloader.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// ImageDownloader.swift
|
||||
// Demo
|
||||
//
|
||||
// Created by John Rommel Estropia on 2020/08/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
// MARK: - ImageDownloader
|
||||
|
||||
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(completion: @escaping (UIImage) -> Void = { _ in }) {
|
||||
|
||||
guard let url = url else {
|
||||
|
||||
return
|
||||
}
|
||||
if let image = Self.cache[url] {
|
||||
|
||||
self.objectWillChange.send()
|
||||
self.image = image
|
||||
completion(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
|
||||
completion(image)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private static var cache: [URL: UIImage] = [:]
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
}
|
||||
@@ -46,67 +46,6 @@ struct NetworkImageView: View {
|
||||
// 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?
|
||||
}
|
||||
private var imageDownloader: ImageDownloader
|
||||
}
|
||||
|
||||
|
||||
@@ -113,9 +113,14 @@ public final class ListPublisher<O: DynamicObject>: Hashable {
|
||||
Calling `addObserver(_:_:)` multiple times on the same observer is safe.
|
||||
|
||||
- parameter observer: an object to become owner of the specified `callback`
|
||||
- parameter notifyInitial: if `true`, the callback is executed immediately with the current publisher state. Otherwise only succeeding updates will notify the observer. Default value is `false`.
|
||||
- parameter callback: the closure to execute when changes occur
|
||||
*/
|
||||
public func addObserver<T: AnyObject>(_ observer: T, _ callback: @escaping (ListPublisher<O>) -> Void) {
|
||||
public func addObserver<T: AnyObject>(
|
||||
_ observer: T,
|
||||
notifyInitial: Bool = false,
|
||||
_ callback: @escaping (ListPublisher<O>) -> Void
|
||||
) {
|
||||
|
||||
Internals.assert(
|
||||
Thread.isMainThread,
|
||||
@@ -125,6 +130,10 @@ public final class ListPublisher<O: DynamicObject>: Hashable {
|
||||
Internals.Closure(callback),
|
||||
forKey: observer
|
||||
)
|
||||
if notifyInitial {
|
||||
|
||||
callback(self)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -83,9 +83,14 @@ public final class ObjectPublisher<O: DynamicObject>: ObjectRepresentation, Hash
|
||||
Calling `addObserver(_:_:)` multiple times on the same observer is safe.
|
||||
|
||||
- parameter observer: an object to become owner of the specified `callback`
|
||||
- parameter notifyInitial: if `true`, the callback is executed immediately with the current publisher state. Otherwise only succeeding updates will notify the observer. Default value is `false`.
|
||||
- parameter callback: the closure to execute when changes occur
|
||||
*/
|
||||
public func addObserver<T: AnyObject>(_ observer: T, _ callback: @escaping (ObjectPublisher<O>) -> Void) {
|
||||
public func addObserver<T: AnyObject>(
|
||||
_ observer: T,
|
||||
notifyInitial: Bool = false,
|
||||
_ callback: @escaping (ObjectPublisher<O>) -> Void
|
||||
) {
|
||||
|
||||
Internals.assert(
|
||||
Thread.isMainThread,
|
||||
@@ -96,6 +101,11 @@ public final class ObjectPublisher<O: DynamicObject>: ObjectRepresentation, Hash
|
||||
forKey: observer
|
||||
)
|
||||
_ = self.lazySnapshot
|
||||
|
||||
if notifyInitial {
|
||||
|
||||
callback(self)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user