improve Pokedex demo

This commit is contained in:
John Estropia
2020-08-29 20:02:05 +09:00
parent 1db91fcec3
commit 1c735a9228
18 changed files with 820 additions and 426 deletions

View File

@@ -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 */,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: {

View File

@@ -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")
]
)
)

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

View File

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

View File

@@ -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)
}
}
/**

View File

@@ -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)
}
}
/**