mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-09-10 03:41:53 +02:00
WIP
This commit is contained in:
@@ -20,6 +20,7 @@ protocol Advanced_EvolutionDemo_CreatureType: DynamicObject, CustomStringConvert
|
||||
|
||||
var dnaCode: Int64 { get set }
|
||||
|
||||
@MainActor
|
||||
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource
|
||||
|
||||
static func count(in transaction: BaseDataTransaction) throws -> Int
|
||||
|
||||
+48
-87
@@ -3,27 +3,29 @@
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
import Combine
|
||||
import Observation
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.CreaturesDataSource
|
||||
|
||||
|
||||
/**
|
||||
A type-erasing adapter to support different `ListPublisher` types
|
||||
*/
|
||||
final class CreaturesDataSource: ObservableObject {
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class CreaturesDataSource {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init<T: NSManagedObject & Advanced.EvolutionDemo.CreatureType>(
|
||||
|
||||
init<T: DynamicObject & Advanced.EvolutionDemo.CreatureType>(
|
||||
listPublisher: ListPublisher<T>,
|
||||
dataStack: DataStack
|
||||
) {
|
||||
|
||||
|
||||
self.numberOfItems = {
|
||||
listPublisher.snapshot.numberOfItems
|
||||
}
|
||||
@@ -31,13 +33,13 @@ extension Advanced.EvolutionDemo {
|
||||
listPublisher.snapshot[index].object?.description
|
||||
}
|
||||
self.addItems = { count in
|
||||
|
||||
|
||||
dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
|
||||
let nextDNACode = try transaction.fetchCount(From<T>())
|
||||
for offset in 0 ..< count {
|
||||
|
||||
|
||||
let object = transaction.create(Into<T>())
|
||||
object.dnaCode = .init(nextDNACode + offset)
|
||||
object.mutate(in: transaction)
|
||||
@@ -47,11 +49,11 @@ extension Advanced.EvolutionDemo {
|
||||
)
|
||||
}
|
||||
self.mutateItemAtIndex = { index in
|
||||
|
||||
|
||||
let object = listPublisher.snapshot[index]
|
||||
dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
|
||||
object
|
||||
.asEditable(in: transaction)?
|
||||
.mutate(in: transaction)
|
||||
@@ -60,109 +62,68 @@ extension Advanced.EvolutionDemo {
|
||||
)
|
||||
}
|
||||
self.deleteAllItems = {
|
||||
|
||||
|
||||
dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
|
||||
try transaction.deleteAll(From<T>())
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
listPublisher.addObserver(self) { [weak self] (listPublisher) in
|
||||
|
||||
self?.objectWillChange.send()
|
||||
listPublisher.addObserver(self) { [weak self] _ in
|
||||
|
||||
Task { @MainActor in
|
||||
self?.refreshID += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init<T: CoreStoreObject & Advanced.EvolutionDemo.CreatureType>(
|
||||
listPublisher: ListPublisher<T>,
|
||||
dataStack: DataStack
|
||||
) {
|
||||
|
||||
self.numberOfItems = {
|
||||
listPublisher.snapshot.numberOfItems
|
||||
}
|
||||
self.itemDescriptionAtIndex = { index in
|
||||
listPublisher.snapshot[index].object?.description
|
||||
}
|
||||
self.addItems = { count in
|
||||
|
||||
dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
let nextDNACode = try transaction.fetchCount(From<T>())
|
||||
for offset in 0 ..< count {
|
||||
|
||||
let object = transaction.create(Into<T>())
|
||||
object.dnaCode = .init(nextDNACode + offset)
|
||||
object.mutate(in: transaction)
|
||||
}
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
self.mutateItemAtIndex = { index in
|
||||
|
||||
let object = listPublisher.snapshot[index]
|
||||
dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
object
|
||||
.asEditable(in: transaction)?
|
||||
.mutate(in: transaction)
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
self.deleteAllItems = {
|
||||
|
||||
dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
try transaction.deleteAll(From<T>())
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
listPublisher.addObserver(self) { [weak self] (listPublisher) in
|
||||
|
||||
self?.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func numberOfCreatures() -> Int {
|
||||
|
||||
|
||||
_ = self.refreshID
|
||||
return self.numberOfItems()
|
||||
}
|
||||
|
||||
|
||||
func creatureDescription(at index: Int) -> String? {
|
||||
|
||||
|
||||
_ = self.refreshID
|
||||
return self.itemDescriptionAtIndex(index)
|
||||
}
|
||||
|
||||
|
||||
func mutate(at index: Int) {
|
||||
|
||||
|
||||
self.mutateItemAtIndex(index)
|
||||
}
|
||||
|
||||
|
||||
func add(count: Int) {
|
||||
|
||||
|
||||
self.addItems(count)
|
||||
}
|
||||
|
||||
|
||||
func clear() {
|
||||
|
||||
|
||||
self.deleteAllItems()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
|
||||
private var refreshID: Int = 0
|
||||
|
||||
@ObservationIgnored
|
||||
private let numberOfItems: () -> Int
|
||||
|
||||
@ObservationIgnored
|
||||
private let itemDescriptionAtIndex: (Int) -> String?
|
||||
|
||||
@ObservationIgnored
|
||||
private let mutateItemAtIndex: (Int) -> Void
|
||||
|
||||
@ObservationIgnored
|
||||
private let addItems: (Int) -> Void
|
||||
|
||||
@ObservationIgnored
|
||||
private let deleteAllItems: () -> Void
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,69 +7,50 @@ import SwiftUI
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.ItemView
|
||||
|
||||
|
||||
struct ItemView: View {
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
|
||||
init(description: String?, mutate: @escaping () -> Void) {
|
||||
|
||||
self.description = description
|
||||
self.mutate = mutate
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
HStack {
|
||||
|
||||
Text(self.description ?? "")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(
|
||||
action: self.mutate,
|
||||
label: {
|
||||
|
||||
Text("Mutate")
|
||||
.foregroundColor(.accentColor)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.disabled(self.description == nil)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
|
||||
fileprivate let description: String?
|
||||
fileprivate let mutate: () -> Void
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Advanced_EvolutionDemo_ItemView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
Advanced.EvolutionDemo.ItemView(
|
||||
description: """
|
||||
dnaCode: 123
|
||||
numberOfLimbs: 4
|
||||
hasVertebrae: true
|
||||
hasHead: true
|
||||
hasTail: true
|
||||
habitat: land
|
||||
hasWings: false
|
||||
""",
|
||||
mutate: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -8,92 +8,51 @@ import SwiftUI
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.ListView
|
||||
|
||||
|
||||
struct ListView: View {
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
let dataSource = self.dataSource
|
||||
return List {
|
||||
ForEach(0 ..< dataSource.numberOfCreatures(), id: \.self) { (index) in
|
||||
Advanced.EvolutionDemo.ItemView(
|
||||
description: dataSource.creatureDescription(at: index),
|
||||
mutate: {
|
||||
|
||||
dataSource.mutate(at: index)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
|
||||
init(
|
||||
period: Advanced.EvolutionDemo.GeologicalPeriod,
|
||||
dataStack: DataStack,
|
||||
dataSource: Advanced.EvolutionDemo.CreaturesDataSource
|
||||
) {
|
||||
|
||||
|
||||
self.period = period
|
||||
self.dataStack = dataStack
|
||||
self.dataSource = dataSource
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let period: Advanced.EvolutionDemo.GeologicalPeriod
|
||||
|
||||
private let dataStack: DataStack
|
||||
|
||||
@ObservedObject
|
||||
private var dataSource: Advanced.EvolutionDemo.CreaturesDataSource
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Advanced_EvolutionDemo_ListView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let dataStack = DataStack(
|
||||
CoreStoreSchema(
|
||||
modelVersion: Advanced.EvolutionDemo.V4.name,
|
||||
entities: [
|
||||
Entity<Advanced.EvolutionDemo.V4.Creature>("Creature")
|
||||
]
|
||||
)
|
||||
)
|
||||
try! dataStack.addStorageAndWait(
|
||||
SQLiteStore(fileName: "Advanced.EvolutionDemo.ListView.Preview.sqlite")
|
||||
)
|
||||
try! dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
for dnaCode in 0 ..< 10 as Range<Int64> {
|
||||
|
||||
let object = transaction.create(Into<Advanced.EvolutionDemo.V4.Creature>())
|
||||
object.dnaCode = dnaCode
|
||||
object.mutate(in: transaction)
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
|
||||
List {
|
||||
ForEach(0 ..< self.dataSource.numberOfCreatures(), id: \.self) { index in
|
||||
|
||||
Advanced.EvolutionDemo.ItemView(
|
||||
description: self.dataSource.creatureDescription(at: index),
|
||||
mutate: {
|
||||
|
||||
self.dataSource.mutate(at: index)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
return Advanced.EvolutionDemo.ListView(
|
||||
period: .ageOfMammals,
|
||||
dataStack: dataStack,
|
||||
dataSource: Advanced.EvolutionDemo.V4.Creature.dataSource(in: dataStack)
|
||||
)
|
||||
.listStyle(.plain)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let period: Advanced.EvolutionDemo.GeologicalPeriod
|
||||
|
||||
private let dataStack: DataStack
|
||||
|
||||
private let dataSource: Advanced.EvolutionDemo.CreaturesDataSource
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -8,71 +8,54 @@ import SwiftUI
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.MainView
|
||||
|
||||
|
||||
struct MainView: View {
|
||||
|
||||
|
||||
@State
|
||||
private var migrator = Advanced.EvolutionDemo.Migrator()
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
|
||||
var body: some View {
|
||||
let migrator = self.migrator
|
||||
let listView: AnyView
|
||||
if let current = migrator.current {
|
||||
|
||||
listView = AnyView(
|
||||
|
||||
VStack(spacing: 0) {
|
||||
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
|
||||
Text("Age of")
|
||||
.padding(.trailing)
|
||||
|
||||
Picker(selection: $migrator.currentPeriod, label: EmptyView()) {
|
||||
|
||||
ForEach(Advanced.EvolutionDemo.GeologicalPeriod.allCases, id: \.self) { period in
|
||||
|
||||
Text(period.description).tag(period)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
.padding()
|
||||
|
||||
if let current = migrator.current {
|
||||
|
||||
Advanced.EvolutionDemo.ListView(
|
||||
period: current.period,
|
||||
dataStack: current.dataStack,
|
||||
dataSource: current.dataSource
|
||||
)
|
||||
)
|
||||
}
|
||||
else {
|
||||
|
||||
listView = AnyView(
|
||||
Advanced.EvolutionDemo.ProgressView(progress: migrator.progress)
|
||||
)
|
||||
}
|
||||
|
||||
return VStack(spacing: 0) {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Text("Age of")
|
||||
.padding(.trailing)
|
||||
Picker(selection: self.$migrator.currentPeriod, label: EmptyView()) {
|
||||
ForEach(Advanced.EvolutionDemo.GeologicalPeriod.allCases, id: \.self) { period in
|
||||
Text(period.description).tag(period)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
}
|
||||
.padding()
|
||||
listView
|
||||
.edgesIgnoringSafeArea(.vertical)
|
||||
.ignoresSafeArea(.container, edges: .vertical)
|
||||
}
|
||||
.navigationBarTitle("Evolution")
|
||||
.disabled(migrator.isBusy || migrator.current == nil)
|
||||
else {
|
||||
|
||||
Advanced.EvolutionDemo.ProgressView(progress: migrator.progress)
|
||||
.ignoresSafeArea(.container, edges: .vertical)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Evolution")
|
||||
.disabled(migrator.isBusy || migrator.current == nil)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@ObservedObject
|
||||
private var migrator: Advanced.EvolutionDemo.Migrator = .init()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Advanced_EvolutionDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Advanced.EvolutionDemo.MainView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import CoreStore
|
||||
import Foundation
|
||||
import Combine
|
||||
import Observation
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
@@ -13,8 +13,10 @@ extension Advanced.EvolutionDemo {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.Migrator
|
||||
|
||||
final class Migrator: ObservableObject {
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class Migrator {
|
||||
|
||||
/**
|
||||
⭐️ Sample 1: Creating a complex `DataStack` that contains all schema histories. The `exactCurrentModelVersion` will specify the target version (if required), and `migrationChain` will provide the upgrade/downgrade progressive migration path.
|
||||
*/
|
||||
@@ -22,7 +24,7 @@ extension Advanced.EvolutionDemo {
|
||||
exactCurrentModelVersion: ModelVersion?,
|
||||
migrationChain: MigrationChain
|
||||
) -> DataStack {
|
||||
|
||||
|
||||
let xcodeV1ToV2ModelSchema = XcodeDataModelSchema.from(
|
||||
modelName: "Advanced.EvolutionDemo.V1",
|
||||
bundle: Bundle(for: Advanced.EvolutionDemo.V1.Creature.self)
|
||||
@@ -49,12 +51,12 @@ extension Advanced.EvolutionDemo {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
⭐️ Sample 2: Creating a complex `SQLiteStore` that contains all schema mappings for both upgrade and downgrade cases.
|
||||
*/
|
||||
private func accessSQLiteStore() -> SQLiteStore {
|
||||
|
||||
|
||||
let upgradeMappings: [SchemaMappingProvider] = [
|
||||
Advanced.EvolutionDemo.V2.FromV1.mapping,
|
||||
Advanced.EvolutionDemo.V3.FromV2.mapping,
|
||||
@@ -72,15 +74,15 @@ extension Advanced.EvolutionDemo {
|
||||
localStorageOptions: []
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
⭐️ Sample 3: Find the model version used by an existing `SQLiteStore`, or just return the latest version if the store is not created yet.
|
||||
*/
|
||||
private func findCurrentVersion() -> ModelVersion {
|
||||
|
||||
|
||||
let allVersions = Advanced.EvolutionDemo.GeologicalPeriod.allCases
|
||||
.map({ $0.version })
|
||||
|
||||
.map(\.version)
|
||||
|
||||
// Since we are only interested in finding current version, we'll assume an upgrading `MigrationChain`
|
||||
let dataStack = self.createDataStack(
|
||||
exactCurrentModelVersion: nil,
|
||||
@@ -89,42 +91,36 @@ extension Advanced.EvolutionDemo {
|
||||
let migrations = try! dataStack.requiredMigrationsForStorage(
|
||||
self.accessSQLiteStore()
|
||||
)
|
||||
|
||||
|
||||
// If no migrations are needed, it means either the store is not created yet, or the store is already at the latest model version. In either case, we already know that the store will use the latest version
|
||||
return migrations.first?.sourceVersion
|
||||
?? allVersions.last!
|
||||
?? allVersions.last!
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
|
||||
var currentPeriod: Advanced.EvolutionDemo.GeologicalPeriod = Advanced.EvolutionDemo.GeologicalPeriod.allCases.last! {
|
||||
|
||||
|
||||
didSet {
|
||||
|
||||
|
||||
self.selectModelVersion(self.currentPeriod)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private(set) var current: (
|
||||
period: Advanced.EvolutionDemo.GeologicalPeriod,
|
||||
dataStack: DataStack,
|
||||
dataSource: Advanced.EvolutionDemo.CreaturesDataSource
|
||||
)? {
|
||||
|
||||
willSet {
|
||||
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
)?
|
||||
|
||||
private(set) var isBusy: Bool = false
|
||||
|
||||
|
||||
private(set) var progress: Progress?
|
||||
|
||||
|
||||
|
||||
|
||||
init() {
|
||||
|
||||
|
||||
self.synchronizeCurrentVersion()
|
||||
}
|
||||
|
||||
@@ -132,11 +128,11 @@ extension Advanced.EvolutionDemo {
|
||||
// MARK: Private
|
||||
|
||||
private func synchronizeCurrentVersion() {
|
||||
|
||||
|
||||
guard
|
||||
let currentPeriod = Advanced.EvolutionDemo.GeologicalPeriod(rawValue: self.findCurrentVersion())
|
||||
else {
|
||||
|
||||
|
||||
self.selectModelVersion(self.currentPeriod)
|
||||
return
|
||||
}
|
||||
@@ -144,106 +140,106 @@ extension Advanced.EvolutionDemo {
|
||||
}
|
||||
|
||||
private func selectModelVersion(_ period: Advanced.EvolutionDemo.GeologicalPeriod) {
|
||||
|
||||
|
||||
let currentPeriod = self.current?.period
|
||||
guard period != currentPeriod else {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
self.objectWillChange.send()
|
||||
|
||||
|
||||
self.isBusy = true
|
||||
|
||||
// explicitly trigger `NSPersistentStore` cleanup by deallocating the `DataStack`
|
||||
self.current = nil
|
||||
|
||||
|
||||
let migrationChain: MigrationChain
|
||||
switch (currentPeriod?.version, period.version) {
|
||||
|
||||
|
||||
case (nil, let newVersion):
|
||||
migrationChain = [newVersion]
|
||||
|
||||
|
||||
case (let currentVersion?, let newVersion):
|
||||
let upgradeMigrationChain = Advanced.EvolutionDemo.GeologicalPeriod.allCases
|
||||
.map({ $0.version })
|
||||
.map(\.version)
|
||||
let currentVersionIndex = upgradeMigrationChain.firstIndex(of: currentVersion)!
|
||||
let newVersionIndex = upgradeMigrationChain.firstIndex(of: newVersion)!
|
||||
|
||||
|
||||
migrationChain = MigrationChain(
|
||||
currentVersionIndex > newVersionIndex
|
||||
? upgradeMigrationChain.reversed()
|
||||
: upgradeMigrationChain
|
||||
? upgradeMigrationChain.reversed()
|
||||
: upgradeMigrationChain
|
||||
)
|
||||
}
|
||||
let dataStack = self.createDataStack(
|
||||
exactCurrentModelVersion: period.version,
|
||||
migrationChain: migrationChain
|
||||
)
|
||||
|
||||
let completion = { [weak self] () -> Void in
|
||||
|
||||
guard let self = self else {
|
||||
|
||||
return
|
||||
}
|
||||
self.objectWillChange.send()
|
||||
defer {
|
||||
|
||||
self.isBusy = false
|
||||
}
|
||||
self.current = (
|
||||
period: period,
|
||||
dataStack: dataStack,
|
||||
dataSource: period.creatureType.dataSource(in: dataStack)
|
||||
)
|
||||
self.currentPeriod = period
|
||||
}
|
||||
|
||||
|
||||
self.progress = dataStack.addStorage(
|
||||
self.accessSQLiteStore(),
|
||||
completion: { [weak self] result in
|
||||
|
||||
guard let self = self else {
|
||||
|
||||
|
||||
guard let self else {
|
||||
|
||||
return
|
||||
}
|
||||
guard case .success = result else {
|
||||
|
||||
self.objectWillChange.send()
|
||||
|
||||
self.isBusy = false
|
||||
return
|
||||
}
|
||||
if self.progress == nil {
|
||||
|
||||
self.spawnCreatures(in: dataStack, period: period, completion: completion)
|
||||
|
||||
self.spawnCreatures(in: dataStack, period: period) { [weak self] in
|
||||
self?.completeSelection(period: period, dataStack: dataStack)
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
completion()
|
||||
|
||||
self.completeSelection(period: period, dataStack: dataStack)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func completeSelection(
|
||||
period: Advanced.EvolutionDemo.GeologicalPeriod,
|
||||
dataStack: DataStack
|
||||
) {
|
||||
defer {
|
||||
|
||||
self.isBusy = false
|
||||
}
|
||||
self.current = (
|
||||
period: period,
|
||||
dataStack: dataStack,
|
||||
dataSource: period.creatureType.dataSource(in: dataStack)
|
||||
)
|
||||
self.currentPeriod = period
|
||||
self.progress = nil
|
||||
}
|
||||
|
||||
private func spawnCreatures(
|
||||
in dataStack: DataStack,
|
||||
period: Advanced.EvolutionDemo.GeologicalPeriod,
|
||||
completion: @escaping () -> Void
|
||||
completion: @escaping @MainActor @Sendable () -> Void
|
||||
) {
|
||||
|
||||
|
||||
dataStack.perform(
|
||||
asynchronous: { (transaction) in
|
||||
|
||||
asynchronous: { transaction in
|
||||
|
||||
let creatureType = period.creatureType
|
||||
for dnaCode in try creatureType.count(in: transaction) ..< 10000 {
|
||||
|
||||
|
||||
let object = creatureType.create(in: transaction)
|
||||
object.dnaCode = Int64(dnaCode)
|
||||
object.mutate(in: transaction)
|
||||
}
|
||||
},
|
||||
completion: { _ in completion() }
|
||||
completion: { _ in
|
||||
|
||||
completion()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+92
-86
@@ -2,125 +2,131 @@
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Observation
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.ProgressView
|
||||
|
||||
|
||||
struct ProgressView: View {
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
|
||||
init(progress: Progress?) {
|
||||
|
||||
self.progressObserver = .init(progress)
|
||||
|
||||
self.progress = progress
|
||||
self._progressObserver = State(initialValue: .init(progress))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
guard self.progressObserver.isMigrating else {
|
||||
|
||||
return AnyView(
|
||||
|
||||
Group {
|
||||
|
||||
if self.progressObserver.isMigrating {
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Text("Migrating: \(self.progressObserver.localizedDescription)")
|
||||
.font(.headline)
|
||||
.padding([.top, .horizontal])
|
||||
|
||||
Text("Progressive step: \(self.progressObserver.localizedAdditionalDescription)")
|
||||
.font(.subheadline)
|
||||
.padding(.horizontal)
|
||||
|
||||
GeometryReader { geometry in
|
||||
|
||||
ZStack(alignment: .leading) {
|
||||
|
||||
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.frame(width: geometry.size.width, height: 8)
|
||||
|
||||
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
||||
.fill(Color.blue)
|
||||
.frame(
|
||||
width: geometry.size.width
|
||||
* self.progressObserver.fractionCompleted,
|
||||
height: 8
|
||||
)
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
else {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Text("Preparing creatures...")
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
)
|
||||
}
|
||||
return AnyView(
|
||||
VStack(alignment: .leading) {
|
||||
Text("Migrating: \(self.progressObserver.localizedDescription)")
|
||||
.font(.headline)
|
||||
.padding([.top, .horizontal])
|
||||
Text("Progressive step: \(self.progressObserver.localizedAdditionalDescription)")
|
||||
.font(.subheadline)
|
||||
.padding(.horizontal)
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.frame(width: geometry.size.width, height: 8)
|
||||
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
||||
.fill(Color.blue)
|
||||
.frame(
|
||||
width: geometry.size.width
|
||||
* self.progressObserver.fractionCompleted,
|
||||
height: 8
|
||||
)
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
)
|
||||
}
|
||||
.task(id: self.progress.map { ObjectIdentifier($0) }) {
|
||||
|
||||
self.progressObserver = .init(self.progress)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
@ObservedObject
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let progress: Progress?
|
||||
|
||||
@State
|
||||
private var progressObserver: ProgressObserver
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: - ProgressObserver
|
||||
|
||||
fileprivate final class ProgressObserver: ObservableObject {
|
||||
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
fileprivate final class ProgressObserver {
|
||||
|
||||
private(set) var fractionCompleted: CGFloat = 0
|
||||
private(set) var localizedDescription: String = ""
|
||||
private(set) var localizedAdditionalDescription: String = ""
|
||||
|
||||
|
||||
var isMigrating: Bool {
|
||||
|
||||
return self.progress != nil
|
||||
|
||||
self.progress != nil
|
||||
}
|
||||
|
||||
|
||||
init(_ progress: Progress?) {
|
||||
|
||||
|
||||
self.progress = progress
|
||||
|
||||
progress?.setProgressHandler { [weak self] (progess) in
|
||||
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
self.objectWillChange.send()
|
||||
self.fractionCompleted = CGFloat(progress?.fractionCompleted ?? 0)
|
||||
self.localizedDescription = progress?.localizedDescription ?? ""
|
||||
self.localizedAdditionalDescription = progress?.localizedAdditionalDescription ?? ""
|
||||
self.syncValues(from: progress)
|
||||
|
||||
progress?.setProgressHandler { [weak self] progress in
|
||||
|
||||
self?.syncValues(from: progress)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
|
||||
@ObservationIgnored
|
||||
private let progress: Progress?
|
||||
|
||||
private func syncValues(from progress: Progress?) {
|
||||
|
||||
self.fractionCompleted = CGFloat(progress?.fractionCompleted ?? 0)
|
||||
self.localizedDescription = progress?.localizedDescription ?? ""
|
||||
self.localizedAdditionalDescription = progress?.localizedAdditionalDescription ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Advanced_EvolutionDemo_ProgressView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
let progress = Progress(totalUnitCount: 10)
|
||||
progress.completedUnitCount = 3
|
||||
return Advanced.EvolutionDemo.ProgressView(
|
||||
progress: progress
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,7 @@ import CoreStore
|
||||
|
||||
@objc(Advanced_EvolutionDemo_V1_Creature)
|
||||
final class Advanced_EvolutionDemo_V1_Creature: NSManagedObject, Advanced.EvolutionDemo.CreatureType {
|
||||
|
||||
|
||||
@NSManaged
|
||||
dynamic var dnaCode: Int64
|
||||
|
||||
@@ -30,9 +30,10 @@ final class Advanced_EvolutionDemo_V1_Creature: NSManagedObject, Advanced.Evolut
|
||||
|
||||
|
||||
// MARK: Advanced.EvolutionDemo.CreatureType
|
||||
|
||||
|
||||
@MainActor
|
||||
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
|
||||
|
||||
|
||||
return .init(
|
||||
listPublisher: dataStack.publishList(
|
||||
From<Advanced.EvolutionDemo.V1.Creature>()
|
||||
@@ -41,16 +42,16 @@ final class Advanced_EvolutionDemo_V1_Creature: NSManagedObject, Advanced.Evolut
|
||||
dataStack: dataStack
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func count(in transaction: BaseDataTransaction) throws -> Int {
|
||||
|
||||
|
||||
return try transaction.fetchCount(
|
||||
From<Advanced.EvolutionDemo.V1.Creature>()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V1.Creature {
|
||||
|
||||
|
||||
return transaction.create(
|
||||
Into<Advanced.EvolutionDemo.V1.Creature>()
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ import CoreStore
|
||||
|
||||
@objc(Advanced_EvolutionDemo_V2_Creature)
|
||||
final class Advanced_EvolutionDemo_V2_Creature: NSManagedObject, Advanced.EvolutionDemo.CreatureType {
|
||||
|
||||
|
||||
@NSManaged
|
||||
dynamic var dnaCode: Int64
|
||||
|
||||
@@ -42,9 +42,10 @@ final class Advanced_EvolutionDemo_V2_Creature: NSManagedObject, Advanced.Evolut
|
||||
|
||||
|
||||
// MARK: Advanced.EvolutionDemo.CreatureType
|
||||
|
||||
|
||||
@MainActor
|
||||
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
|
||||
|
||||
|
||||
return .init(
|
||||
listPublisher: dataStack.publishList(
|
||||
From<Advanced.EvolutionDemo.V2.Creature>()
|
||||
@@ -53,16 +54,16 @@ final class Advanced_EvolutionDemo_V2_Creature: NSManagedObject, Advanced.Evolut
|
||||
dataStack: dataStack
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func count(in transaction: BaseDataTransaction) throws -> Int {
|
||||
|
||||
|
||||
return try transaction.fetchCount(
|
||||
From<Advanced.EvolutionDemo.V2.Creature>()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V2.Creature {
|
||||
|
||||
|
||||
return transaction.create(
|
||||
Into<Advanced.EvolutionDemo.V2.Creature>()
|
||||
)
|
||||
|
||||
+16
-15
@@ -8,31 +8,31 @@ import CoreStore
|
||||
// MARK: - Advanced.EvolutionDemo.V3
|
||||
|
||||
extension Advanced.EvolutionDemo.V3 {
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V3.Creature
|
||||
|
||||
|
||||
final class Creature: CoreStoreObject, Advanced.EvolutionDemo.CreatureType {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
|
||||
@Field.Stored("dnaCode")
|
||||
var dnaCode: Int64 = 0
|
||||
|
||||
|
||||
@Field.Stored("numberOfLimbs")
|
||||
var numberOfLimbs: Int32 = 0
|
||||
|
||||
|
||||
@Field.Stored("hasVertebrae")
|
||||
var hasVertebrae: Bool = false
|
||||
|
||||
|
||||
@Field.Stored("hasHead")
|
||||
var hasHead: Bool = true
|
||||
|
||||
|
||||
@Field.Stored("hasTail")
|
||||
var hasTail: Bool = true
|
||||
|
||||
|
||||
@Field.Stored("hasWings")
|
||||
var hasWings: Bool = false
|
||||
|
||||
|
||||
@Field.Stored("habitat")
|
||||
var habitat: Habitat = .water
|
||||
|
||||
@@ -64,9 +64,10 @@ extension Advanced.EvolutionDemo.V3 {
|
||||
|
||||
|
||||
// MARK: Advanced.EvolutionDemo.CreatureType
|
||||
|
||||
|
||||
@MainActor
|
||||
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
|
||||
|
||||
|
||||
return .init(
|
||||
listPublisher: dataStack.publishList(
|
||||
From<Advanced.EvolutionDemo.V3.Creature>()
|
||||
@@ -75,16 +76,16 @@ extension Advanced.EvolutionDemo.V3 {
|
||||
dataStack: dataStack
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func count(in transaction: BaseDataTransaction) throws -> Int {
|
||||
|
||||
|
||||
return try transaction.fetchCount(
|
||||
From<Advanced.EvolutionDemo.V3.Creature>()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V3.Creature {
|
||||
|
||||
|
||||
return transaction.create(
|
||||
Into<Advanced.EvolutionDemo.V3.Creature>()
|
||||
)
|
||||
|
||||
+17
-16
@@ -8,37 +8,37 @@ import CoreStore
|
||||
// MARK: - Advanced.EvolutionDemo.V4
|
||||
|
||||
extension Advanced.EvolutionDemo.V4 {
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V4.Creature
|
||||
|
||||
|
||||
final class Creature: CoreStoreObject, Advanced.EvolutionDemo.CreatureType {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
|
||||
@Field.Stored("dnaCode")
|
||||
var dnaCode: Int64 = 0
|
||||
|
||||
|
||||
@Field.Stored("numberOfLimbs")
|
||||
var numberOfLimbs: Int32 = 0
|
||||
|
||||
|
||||
@Field.Stored("hasVertebrae")
|
||||
var hasVertebrae: Bool = false
|
||||
|
||||
|
||||
@Field.Stored("hasHead")
|
||||
var hasHead: Bool = true
|
||||
|
||||
|
||||
@Field.Stored("hasTail")
|
||||
var hasTail: Bool = false
|
||||
|
||||
|
||||
@Field.Stored("hasWings")
|
||||
var hasWings: Bool = false
|
||||
|
||||
|
||||
typealias Habitat = Advanced.EvolutionDemo.V3.Creature.Habitat
|
||||
|
||||
|
||||
@Field.Stored("habitat")
|
||||
var habitat: Habitat = .water
|
||||
|
||||
|
||||
@Field.Stored("isWarmBlooded")
|
||||
var isWarmBlooded: Bool = true
|
||||
|
||||
@@ -60,9 +60,10 @@ extension Advanced.EvolutionDemo.V4 {
|
||||
|
||||
|
||||
// MARK: Advanced.EvolutionDemo.CreatureType
|
||||
|
||||
|
||||
@MainActor
|
||||
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
|
||||
|
||||
|
||||
return .init(
|
||||
listPublisher: dataStack.publishList(
|
||||
From<Advanced.EvolutionDemo.V4.Creature>()
|
||||
@@ -71,16 +72,16 @@ extension Advanced.EvolutionDemo.V4 {
|
||||
dataStack: dataStack
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func count(in transaction: BaseDataTransaction) throws -> Int {
|
||||
|
||||
|
||||
return try transaction.fetchCount(
|
||||
From<Advanced.EvolutionDemo.V4.Creature>()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V4.Creature {
|
||||
|
||||
|
||||
return transaction.create(
|
||||
Into<Advanced.EvolutionDemo.V4.Creature>()
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
|
||||
@@ -22,19 +21,19 @@ extension Classic.ColorsDemo {
|
||||
}
|
||||
|
||||
// MARK: UIViewControllerRepresentable
|
||||
|
||||
|
||||
typealias UIViewControllerType = Classic.ColorsDemo.DetailViewController
|
||||
|
||||
|
||||
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
|
||||
|
||||
return UIViewControllerType(self.palette)
|
||||
}
|
||||
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
|
||||
|
||||
uiViewController.palette = self.palette
|
||||
}
|
||||
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||
|
||||
func makeCoordinator() -> ObjectMonitor<Classic.ColorsDemo.Palette> {
|
||||
@@ -48,32 +47,3 @@ extension Classic.ColorsDemo {
|
||||
private let palette: ObjectMonitor<Classic.ColorsDemo.Palette>
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Classic_ColorsDemo_DetailView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
try! Classic.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
guard (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>())) <= 0 else {
|
||||
return
|
||||
}
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
)
|
||||
|
||||
return Classic.ColorsDemo.DetailView(
|
||||
Classic.ColorsDemo.dataStack.monitorObject(
|
||||
Classic.ColorsDemo.palettesMonitor[0, 0]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -55,35 +55,29 @@ extension Classic.ColorsDemo {
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Classic_ColorsDemo_ListView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Classic.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
// MARK: - Preview
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Classic.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Classic.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
#Preview {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Classic.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Classic.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
)
|
||||
return Classic.ColorsDemo.ListView(
|
||||
listMonitor: Classic.ColorsDemo.palettesMonitor,
|
||||
onPaletteTapped: { _ in }
|
||||
)
|
||||
}
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
#endif
|
||||
let palette = transaction.create(Into<Classic.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
}
|
||||
)
|
||||
return Classic.ColorsDemo.ListView(
|
||||
listMonitor: Classic.ColorsDemo.palettesMonitor,
|
||||
onPaletteTapped: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
import Observation
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Classic.ColorsDemo
|
||||
@@ -12,97 +13,85 @@ extension Classic.ColorsDemo {
|
||||
// MARK: - Classic.ColorsDemo.MainView
|
||||
|
||||
struct MainView: View {
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
|
||||
init() {
|
||||
|
||||
|
||||
let listMonitor = Classic.ColorsDemo.palettesMonitor
|
||||
self.listMonitor = listMonitor
|
||||
self.listHelper = .init(listMonitor: listMonitor)
|
||||
self._listHelper = State(initialValue: .init(listMonitor: listMonitor))
|
||||
self._filter = Binding(
|
||||
get: { Classic.ColorsDemo.filter },
|
||||
set: { Classic.ColorsDemo.filter = $0 }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
|
||||
var body: some View {
|
||||
let detailView: AnyView
|
||||
if let selectedObject = self.listHelper.selectedObject() {
|
||||
|
||||
detailView = AnyView(
|
||||
Classic.ColorsDemo.DetailView(selectedObject)
|
||||
VStack(spacing: 0) {
|
||||
Classic.ColorsDemo.ListView(
|
||||
listMonitor: self.listMonitor,
|
||||
onPaletteTapped: {
|
||||
|
||||
self.listHelper.setSelectedPalette($0)
|
||||
}
|
||||
)
|
||||
}
|
||||
else {
|
||||
|
||||
detailView = AnyView(EmptyView())
|
||||
}
|
||||
let listMonitor = self.listMonitor
|
||||
return VStack(spacing: 0) {
|
||||
Classic.ColorsDemo.ListView
|
||||
.init(
|
||||
listMonitor: listMonitor,
|
||||
onPaletteTapped: {
|
||||
|
||||
self.listHelper.setSelectedPalette($0)
|
||||
}
|
||||
)
|
||||
.navigationBarTitle(
|
||||
Text("Colors (\(self.listHelper.count) objects)")
|
||||
)
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
.edgesIgnoringSafeArea(.vertical)
|
||||
detailView
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
}
|
||||
.navigationBarItems(
|
||||
leading: HStack {
|
||||
EditButton()
|
||||
Button(
|
||||
action: { self.clearColors() },
|
||||
label: { Text("Clear") }
|
||||
)
|
||||
},
|
||||
trailing: HStack {
|
||||
Button(
|
||||
action: { self.changeFilter() },
|
||||
label: { Text(self.filter.rawValue) }
|
||||
)
|
||||
Button(
|
||||
action: { self.shuffleColors() },
|
||||
label: { Text("Shuffle") }
|
||||
)
|
||||
Button(
|
||||
action: { self.addColor() },
|
||||
label: { Text("Add") }
|
||||
)
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
.ignoresSafeArea(.container, edges: .vertical)
|
||||
|
||||
if let selectedObject = self.listHelper.selectedObject() {
|
||||
Classic.ColorsDemo.DetailView(selectedObject)
|
||||
.ignoresSafeArea()
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
}
|
||||
)
|
||||
}
|
||||
.navigationTitle("Colors (\(self.listHelper.count) objects)")
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .topBarLeading) {
|
||||
EditButton()
|
||||
Button("Clear") {
|
||||
|
||||
self.clearColors()
|
||||
}
|
||||
}
|
||||
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||
Button(self.filter.rawValue) {
|
||||
|
||||
self.changeFilter()
|
||||
}
|
||||
Button("Shuffle") {
|
||||
|
||||
self.shuffleColors()
|
||||
}
|
||||
Button("Add") {
|
||||
|
||||
self.addColor()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
|
||||
private let listMonitor: ListMonitor<Classic.ColorsDemo.Palette>
|
||||
|
||||
@ObservedObject
|
||||
|
||||
@State
|
||||
private var listHelper: ListHelper
|
||||
|
||||
|
||||
@Binding
|
||||
private var filter: Classic.ColorsDemo.Filter
|
||||
|
||||
|
||||
private func changeFilter() {
|
||||
|
||||
|
||||
Classic.ColorsDemo.filter = Classic.ColorsDemo.filter.next()
|
||||
}
|
||||
|
||||
|
||||
private func clearColors() {
|
||||
|
||||
|
||||
Classic.ColorsDemo.dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
@@ -111,7 +100,7 @@ extension Classic.ColorsDemo {
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func addColor() {
|
||||
|
||||
Classic.ColorsDemo.dataStack.perform(
|
||||
@@ -122,7 +111,7 @@ extension Classic.ColorsDemo {
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func shuffleColors() {
|
||||
|
||||
Classic.ColorsDemo.dataStack.perform(
|
||||
@@ -136,112 +125,93 @@ extension Classic.ColorsDemo {
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: - Classic.ColorsDemo.MainView.ListHelper
|
||||
|
||||
fileprivate final class ListHelper: ObservableObject, ListObjectObserver {
|
||||
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
fileprivate final class ListHelper: ListObjectObserver {
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
|
||||
fileprivate private(set) var count: Int = 0
|
||||
|
||||
|
||||
fileprivate init(listMonitor: ListMonitor<Classic.ColorsDemo.Palette>) {
|
||||
|
||||
|
||||
listMonitor.addObserver(self)
|
||||
self.count = listMonitor.numberOfObjects()
|
||||
}
|
||||
|
||||
|
||||
fileprivate func selectedObject() -> ObjectMonitor<Classic.ColorsDemo.Palette>? {
|
||||
|
||||
return self.selectedPalette.flatMap {
|
||||
|
||||
|
||||
self.selectedPalette.flatMap {
|
||||
guard !$0.isDeleted else {
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
return Classic.ColorsDemo.dataStack.monitorObject($0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate func setSelectedPalette(_ palette: Classic.ColorsDemo.Palette?) {
|
||||
|
||||
|
||||
guard self.selectedPalette != palette else {
|
||||
|
||||
|
||||
return
|
||||
}
|
||||
self.objectWillChange.send()
|
||||
if let palette = palette, !palette.isDeleted {
|
||||
|
||||
if let palette, !palette.isDeleted {
|
||||
|
||||
self.selectedPalette = palette
|
||||
}
|
||||
else {
|
||||
|
||||
|
||||
self.selectedPalette = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: ListObserver
|
||||
|
||||
typealias ListEntityType = Classic.ColorsDemo.Palette
|
||||
|
||||
func listMonitorDidChange(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>) {
|
||||
|
||||
self.objectWillChange.send()
|
||||
self.count = monitor.numberOfObjects()
|
||||
}
|
||||
|
||||
func listMonitorDidRefetch(_ monitor: ListMonitor<ListEntityType>) {
|
||||
|
||||
self.objectWillChange.send()
|
||||
self.count = monitor.numberOfObjects()
|
||||
}
|
||||
|
||||
// MARK: ListObjectObserver
|
||||
|
||||
func listMonitor(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>, didDeleteObject object: Classic.ColorsDemo.Palette, fromIndexPath indexPath: IndexPath) {
|
||||
|
||||
if self.selectedPalette == object {
|
||||
|
||||
self.setSelectedPalette(nil)
|
||||
|
||||
// MARK: ListObserver
|
||||
|
||||
typealias ListEntityType = Classic.ColorsDemo.Palette
|
||||
|
||||
nonisolated func listMonitorDidChange(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>) {
|
||||
let count = monitor.numberOfObjects()
|
||||
|
||||
Task { @MainActor in
|
||||
self.count = count
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
nonisolated func listMonitorDidRefetch(_ monitor: ListMonitor<ListEntityType>) {
|
||||
let count = monitor.numberOfObjects()
|
||||
|
||||
Task { @MainActor in
|
||||
self.count = count
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: ListObjectObserver
|
||||
|
||||
nonisolated func listMonitor(
|
||||
_ monitor: ListMonitor<Classic.ColorsDemo.Palette>,
|
||||
didDeleteObject object: Classic.ColorsDemo.Palette,
|
||||
fromIndexPath indexPath: IndexPath
|
||||
) {
|
||||
let deletedObjectURI = object.objectID.uriRepresentation()
|
||||
|
||||
Task { @MainActor in
|
||||
if self.selectedPalette?.objectID.uriRepresentation() == deletedObjectURI {
|
||||
|
||||
self.setSelectedPalette(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
|
||||
private var selectedPalette: Classic.ColorsDemo.Palette?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Classic_ColorsDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Classic.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Classic.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Classic.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
}
|
||||
)
|
||||
return Classic.ColorsDemo.MainView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -38,6 +38,7 @@ extension Classic {
|
||||
return dataStack
|
||||
}()
|
||||
|
||||
@MainActor
|
||||
static let palettesMonitor: ListMonitor<Classic.ColorsDemo.Palette> = Classic.ColorsDemo.dataStack.monitorSectionedList(
|
||||
From<Classic.ColorsDemo.Palette>()
|
||||
.sectionBy(\.colorGroup)
|
||||
@@ -45,6 +46,7 @@ extension Classic {
|
||||
.orderBy(.ascending(\.hue))
|
||||
)
|
||||
|
||||
@MainActor
|
||||
static var filter: Classic.ColorsDemo.Filter = .all {
|
||||
|
||||
didSet {
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
extension Modern.ColorsDemo {
|
||||
|
||||
// MARK: - Modern.ColorsDemo.MainView
|
||||
|
||||
|
||||
struct MainView<ListView: View, DetailView: View>: View {
|
||||
|
||||
// MARK: Internal
|
||||
@@ -20,7 +20,8 @@ extension Modern.ColorsDemo {
|
||||
_ listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
|
||||
_ onPaletteTapped: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
|
||||
) -> ListView,
|
||||
detailView: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> DetailView) {
|
||||
detailView: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> DetailView
|
||||
) {
|
||||
|
||||
self.listView = listView
|
||||
self.detailView = detailView
|
||||
@@ -30,42 +31,51 @@ extension Modern.ColorsDemo {
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
return VStack(spacing: 0) {
|
||||
self.listView(self.$palettes, { self.selectedPalette = $0 })
|
||||
.navigationBarTitle(
|
||||
Text("Colors (\(self.palettes.count) objects)")
|
||||
)
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
self.selectedPalette.map {
|
||||
|
||||
VStack(spacing: 0) {
|
||||
|
||||
self.listView(
|
||||
self.$palettes,
|
||||
{
|
||||
self.selectedPalette = $0
|
||||
}
|
||||
)
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
|
||||
if let selectedPalette = self.selectedPalette {
|
||||
|
||||
self.detailView($0)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
self.detailView(selectedPalette)
|
||||
.ignoresSafeArea()
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
.navigationBarItems(
|
||||
leading: HStack {
|
||||
.navigationTitle("Colors (\(self.palettes.count) objects)")
|
||||
.toolbar {
|
||||
|
||||
ToolbarItemGroup(placement: .topBarLeading) {
|
||||
EditButton()
|
||||
Button(
|
||||
action: { self.clearColors() },
|
||||
label: { Text("Clear") }
|
||||
)
|
||||
},
|
||||
trailing: HStack {
|
||||
Button(
|
||||
action: { self.changeFilter() },
|
||||
label: { Text(self.filter.rawValue) }
|
||||
)
|
||||
Button(
|
||||
action: { self.shuffleColors() },
|
||||
label: { Text("Shuffle") }
|
||||
)
|
||||
Button(
|
||||
action: { self.addColor() },
|
||||
label: { Text("Add") }
|
||||
)
|
||||
Button("Clear") {
|
||||
|
||||
self.clearColors()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||
|
||||
Button(self.filter.rawValue) {
|
||||
|
||||
self.changeFilter()
|
||||
}
|
||||
Button("Shuffle") {
|
||||
|
||||
self.shuffleColors()
|
||||
}
|
||||
Button("Add") {
|
||||
|
||||
self.addColor()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +109,7 @@ extension Modern.ColorsDemo {
|
||||
|
||||
Modern.ColorsDemo.dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
|
||||
try transaction.deleteAll(From<Modern.ColorsDemo.Palette>())
|
||||
},
|
||||
sourceIdentifier: TransactionSource.clear,
|
||||
@@ -108,10 +118,10 @@ extension Modern.ColorsDemo {
|
||||
}
|
||||
|
||||
private func addColor() {
|
||||
|
||||
|
||||
Modern.ColorsDemo.dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
|
||||
_ = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
},
|
||||
sourceIdentifier: TransactionSource.add,
|
||||
@@ -120,12 +130,12 @@ extension Modern.ColorsDemo {
|
||||
}
|
||||
|
||||
private func shuffleColors() {
|
||||
|
||||
|
||||
Modern.ColorsDemo.dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
|
||||
for palette in try transaction.fetchAll(From<Modern.ColorsDemo.Palette>()) {
|
||||
|
||||
|
||||
palette.setRandomHue()
|
||||
}
|
||||
},
|
||||
@@ -136,42 +146,38 @@ extension Modern.ColorsDemo {
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_ColorsDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
// MARK: - Preview
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
#Preview {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
)
|
||||
return Modern.ColorsDemo.MainView(
|
||||
listView: { listPublisher, onPaletteTapped in
|
||||
Modern.ColorsDemo.SwiftUI.ListView(
|
||||
listPublisher: listPublisher,
|
||||
onPaletteTapped: onPaletteTapped
|
||||
)
|
||||
},
|
||||
detailView: { objectPublisher in
|
||||
Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher)
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
return Modern.ColorsDemo.MainView(
|
||||
listView: { listPublisher, onPaletteTapped in
|
||||
|
||||
Modern.ColorsDemo.SwiftUI.ListView(
|
||||
listPublisher: listPublisher,
|
||||
onPaletteTapped: onPaletteTapped
|
||||
)
|
||||
},
|
||||
detailView: { objectPublisher in
|
||||
|
||||
Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
|
||||
@@ -31,9 +30,9 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
|
||||
@Binding
|
||||
private var brightness: Float
|
||||
|
||||
|
||||
init(_ palette: ObjectPublisher<Modern.ColorsDemo.Palette>) {
|
||||
|
||||
|
||||
self._palette = .init(palette)
|
||||
self._hue = Binding(
|
||||
get: { palette.hue ?? 0 },
|
||||
@@ -78,7 +77,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
@@ -87,12 +86,17 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
if let palette = self.palette {
|
||||
|
||||
ZStack(alignment: .center) {
|
||||
|
||||
Color(palette.$color)
|
||||
|
||||
ZStack {
|
||||
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(Color.white)
|
||||
.shadow(color: Color(.sRGB, white: 0.5, opacity: 0.3), radius: 2, x: 1, y: 1)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
|
||||
HStack {
|
||||
Text("H: \(Int(palette.$hue * 359))°")
|
||||
.frame(width: 80)
|
||||
@@ -102,6 +106,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
step: 1 / 359
|
||||
)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("S: \(Int(palette.$saturation * 100))%")
|
||||
.frame(width: 80)
|
||||
@@ -111,6 +116,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
step: 1 / 100
|
||||
)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("B: \(Int(palette.$brightness * 100))%")
|
||||
.frame(width: 80)
|
||||
@@ -131,30 +137,3 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_ColorsDemo_SwiftUI_DetailView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
guard (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>())) <= 0 else {
|
||||
return
|
||||
}
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
)
|
||||
|
||||
return Modern.ColorsDemo.SwiftUI.DetailView(
|
||||
Modern.ColorsDemo.palettesPublisher.snapshot.first!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -34,41 +34,15 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
|
||||
if let palette = self.palette {
|
||||
|
||||
Color(palette.$color).overlay(
|
||||
Text(palette.$colorText)
|
||||
.foregroundColor(palette.$brightness > 0.6 ? .black : .white)
|
||||
.padding(),
|
||||
alignment: .leading
|
||||
)
|
||||
.animation(.default, value: palette)
|
||||
Color(palette.$color)
|
||||
.overlay(
|
||||
Text(palette.$colorText)
|
||||
.foregroundColor(palette.$brightness > 0.6 ? .black : .white)
|
||||
.padding(),
|
||||
alignment: .leading
|
||||
)
|
||||
.animation(.default, value: palette)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_ColorsDemo_SwiftUI_ItemView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
guard (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>())) <= 0 else {
|
||||
return
|
||||
}
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
)
|
||||
|
||||
return Modern.ColorsDemo.SwiftUI.ItemView(
|
||||
Modern.ColorsDemo.palettesPublisher.snapshot.first!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
extension Modern.ColorsDemo.SwiftUI {
|
||||
|
||||
// MARK: - Modern.ColorsDemo.SwiftUI.ListView
|
||||
|
||||
|
||||
struct ListView: View {
|
||||
|
||||
/**
|
||||
@@ -40,7 +40,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
|
||||
ForEach(sectionIn: self.palettes) { section in
|
||||
|
||||
Section(header: Text(section.sectionID)) {
|
||||
Section(section.sectionID) {
|
||||
|
||||
ForEach(objectIn: section) { palette in
|
||||
|
||||
@@ -64,8 +64,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
}
|
||||
}
|
||||
// .animation(.default) // breaks layout
|
||||
.listStyle(PlainListStyle())
|
||||
.edgesIgnoringSafeArea([])
|
||||
.listStyle(.plain)
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +80,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
)
|
||||
Modern.ColorsDemo.dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
|
||||
transaction.delete(objectIDs: objectIDsToDelete)
|
||||
},
|
||||
completion: { _ in }
|
||||
@@ -90,35 +89,29 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_ColorsDemo_SwiftUI_ListView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
// MARK: - Preview
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
#Preview {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
)
|
||||
return Modern.ColorsDemo.SwiftUI.ListView(
|
||||
listPublisher: Modern.ColorsDemo.palettesPublisher,
|
||||
onPaletteTapped: { _ in }
|
||||
)
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
}
|
||||
)
|
||||
return Modern.ColorsDemo.SwiftUI.ListView(
|
||||
listPublisher: Modern.ColorsDemo.palettesPublisher,
|
||||
onPaletteTapped: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
|
||||
@@ -22,21 +21,21 @@ extension Modern.ColorsDemo.UIKit {
|
||||
}
|
||||
|
||||
// MARK: UIViewControllerRepresentable
|
||||
|
||||
|
||||
typealias UIViewControllerType = Modern.ColorsDemo.UIKit.DetailViewController
|
||||
|
||||
|
||||
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
|
||||
|
||||
return UIViewControllerType(self.palette)
|
||||
}
|
||||
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
|
||||
|
||||
uiViewController.palette = Modern.ColorsDemo.dataStack.monitorObject(
|
||||
self.palette.object!
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||
|
||||
|
||||
@@ -45,30 +44,3 @@ extension Modern.ColorsDemo.UIKit {
|
||||
private var palette: ObjectPublisher<Modern.ColorsDemo.Palette>
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_ColorsDemo_UIKit_DetailView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
guard (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>())) <= 0 else {
|
||||
return
|
||||
}
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
)
|
||||
|
||||
return Modern.ColorsDemo.UIKit.DetailView(
|
||||
Modern.ColorsDemo.palettesPublisher.snapshot.first!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
+10
-4
@@ -49,7 +49,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
isolated deinit {
|
||||
|
||||
self.palette.removeObserver(self)
|
||||
}
|
||||
@@ -88,14 +88,17 @@ extension Modern.ColorsDemo.UIKit {
|
||||
|
||||
// MARK: ObjectObserver
|
||||
|
||||
func objectMonitor(
|
||||
nonisolated func objectMonitor(
|
||||
_ monitor: ObjectMonitor<Modern.ColorsDemo.Palette>,
|
||||
didUpdateObject object: Modern.ColorsDemo.Palette,
|
||||
didUpdateObject object: sending Modern.ColorsDemo.Palette,
|
||||
changedPersistentKeys: Set<KeyPathString>,
|
||||
sourceIdentifier: Any?
|
||||
) {
|
||||
|
||||
self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys)
|
||||
MainActor.assumeIsolated {
|
||||
|
||||
self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +248,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
private let saturationSlider: UISlider = .init()
|
||||
private let brightnessSlider: UISlider = .init()
|
||||
|
||||
@MainActor
|
||||
@objc
|
||||
private dynamic func hueSliderValueDidChange(_ sender: UISlider) {
|
||||
|
||||
@@ -259,6 +263,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@objc
|
||||
private dynamic func saturationSliderValueDidChange(_ sender: UISlider) {
|
||||
|
||||
@@ -273,6 +278,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@objc
|
||||
private dynamic func brightnessSliderValueDidChange(_ sender: UISlider) {
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
extension Modern.ColorsDemo.UIKit {
|
||||
|
||||
// MARK: - Modern.ColorsDemo.UIKit.ListView
|
||||
|
||||
|
||||
struct ListView: UIViewControllerRepresentable {
|
||||
|
||||
// MARK: Internal
|
||||
@@ -26,9 +26,9 @@ extension Modern.ColorsDemo.UIKit {
|
||||
|
||||
|
||||
// MARK: UIViewControllerRepresentable
|
||||
|
||||
|
||||
typealias UIViewControllerType = Modern.ColorsDemo.UIKit.ListViewController
|
||||
|
||||
|
||||
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
|
||||
|
||||
return UIViewControllerType(
|
||||
@@ -36,7 +36,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
onPaletteTapped: self.onPaletteTapped
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
|
||||
|
||||
uiViewController.setEditing(
|
||||
@@ -44,7 +44,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||
|
||||
|
||||
@@ -55,35 +55,29 @@ extension Modern.ColorsDemo.UIKit {
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_ColorsDemo_UIKit_ListView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
// MARK: - Preview
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
#Preview {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
)
|
||||
return Modern.ColorsDemo.UIKit.ListView(
|
||||
listPublisher: Modern.ColorsDemo.palettesPublisher,
|
||||
onPaletteTapped: { _ in }
|
||||
)
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
}
|
||||
)
|
||||
return Modern.ColorsDemo.UIKit.ListView(
|
||||
listPublisher: Modern.ColorsDemo.palettesPublisher,
|
||||
onPaletteTapped: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
+14
-14
@@ -42,13 +42,13 @@ extension Modern.ColorsDemo.UIKit {
|
||||
switch transactionSource as? Modern.ColorsDemo.TransactionSource {
|
||||
|
||||
case .add,
|
||||
.delete,
|
||||
.shuffle,
|
||||
.clear:
|
||||
.delete,
|
||||
.shuffle,
|
||||
.clear:
|
||||
dataSource.apply(listPublisher.snapshot, animatingDifferences: true)
|
||||
|
||||
case nil,
|
||||
.refetch:
|
||||
.refetch:
|
||||
dataSource.apply(listPublisher.snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
@@ -57,22 +57,22 @@ extension Modern.ColorsDemo.UIKit {
|
||||
/**
|
||||
⭐️ Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ListPublisher`s safely remove deallocated observers automatically.
|
||||
*/
|
||||
deinit {
|
||||
isolated deinit {
|
||||
|
||||
self.listPublisher.removeObserver(self)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
⭐️ Sample 4: This is the custom `DiffableDataSource.TableViewAdapter` subclass we wrote that enabled swipe-to-delete gestures and section index titles on the `UITableView`.
|
||||
*/
|
||||
final class CustomDataSource: DiffableDataSource.TableViewAdapter<Modern.ColorsDemo.Palette> {
|
||||
|
||||
// MARK: UITableViewDataSource
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
|
||||
|
||||
|
||||
switch editingStyle {
|
||||
|
||||
|
||||
case .delete:
|
||||
guard let itemID = self.itemID(for: indexPath) else {
|
||||
|
||||
@@ -80,13 +80,13 @@ extension Modern.ColorsDemo.UIKit {
|
||||
}
|
||||
self.dataStack.perform(
|
||||
asynchronous: { (transaction) in
|
||||
|
||||
|
||||
transaction.delete(objectIDs: [itemID])
|
||||
},
|
||||
sourceIdentifier: Modern.ColorsDemo.TransactionSource.delete,
|
||||
completion: { _ in }
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -116,10 +116,10 @@ extension Modern.ColorsDemo.UIKit {
|
||||
|
||||
super.init(style: .plain)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: UIViewController
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
super.viewDidLoad()
|
||||
@@ -128,7 +128,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
Modern.ColorsDemo.UIKit.ItemCell.self,
|
||||
forCellReuseIdentifier: Modern.ColorsDemo.UIKit.ItemCell.reuseIdentifier
|
||||
)
|
||||
|
||||
|
||||
self.startObservingList()
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ extension Modern {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
@MainActor
|
||||
static let dataStack: DataStack = {
|
||||
|
||||
let dataStack = DataStack(
|
||||
@@ -44,6 +45,7 @@ extension Modern {
|
||||
return dataStack
|
||||
}()
|
||||
|
||||
@MainActor
|
||||
static let palettesPublisher: ListPublisher<Modern.ColorsDemo.Palette> = Modern.ColorsDemo.dataStack.publishList(
|
||||
From<Modern.ColorsDemo.Palette>()
|
||||
.sectionBy(
|
||||
@@ -54,6 +56,7 @@ extension Modern {
|
||||
.orderBy(.ascending(\.$hue))
|
||||
)
|
||||
|
||||
@MainActor
|
||||
static var filter: Modern.ColorsDemo.Filter = .all {
|
||||
|
||||
didSet {
|
||||
|
||||
@@ -13,51 +13,58 @@ extension Modern.PlacemarksDemo {
|
||||
|
||||
// MARK: Geocoder
|
||||
|
||||
@MainActor
|
||||
final class Geocoder {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
func geocode(
|
||||
place: ObjectSnapshot<Modern.PlacemarksDemo.Place>,
|
||||
completion: @escaping (_ title: String?, _ subtitle: String?) -> Void
|
||||
) {
|
||||
place: ObjectSnapshot<Modern.PlacemarksDemo.Place>
|
||||
) async -> (title: String?, subtitle: String?) {
|
||||
|
||||
self.geocoder?.cancelGeocode()
|
||||
|
||||
let geocoder = CLGeocoder()
|
||||
self.geocoder = geocoder
|
||||
geocoder.reverseGeocodeLocation(
|
||||
CLLocation(latitude: place.$latitude, longitude: place.$longitude),
|
||||
completionHandler: { (placemarks, error) -> Void in
|
||||
|
||||
defer {
|
||||
|
||||
self.geocoder = nil
|
||||
}
|
||||
guard let placemark = placemarks?.first else {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let address = CNMutablePostalAddress()
|
||||
address.street = placemark.thoroughfare ?? ""
|
||||
address.subLocality = placemark.subThoroughfare ?? ""
|
||||
address.city = placemark.locality ?? ""
|
||||
address.subAdministrativeArea = placemark.subAdministrativeArea ?? ""
|
||||
address.state = placemark.administrativeArea ?? ""
|
||||
address.postalCode = placemark.postalCode ?? ""
|
||||
address.country = placemark.country ?? ""
|
||||
address.isoCountryCode = placemark.isoCountryCode ?? ""
|
||||
|
||||
completion(
|
||||
placemark.name,
|
||||
CNPostalAddressFormatter.string(
|
||||
from: address,
|
||||
style: .mailingAddress
|
||||
)
|
||||
)
|
||||
|
||||
defer {
|
||||
|
||||
if self.geocoder === geocoder {
|
||||
self.geocoder = nil
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
do {
|
||||
let placemarks = try await geocoder.reverseGeocodeLocation(
|
||||
CLLocation(latitude: place.$latitude, longitude: place.$longitude)
|
||||
)
|
||||
guard let placemark = placemarks.first else {
|
||||
|
||||
return (nil, nil)
|
||||
}
|
||||
|
||||
let address = CNMutablePostalAddress()
|
||||
address.street = placemark.thoroughfare ?? ""
|
||||
address.subLocality = placemark.subThoroughfare ?? ""
|
||||
address.city = placemark.locality ?? ""
|
||||
address.subAdministrativeArea = placemark.subAdministrativeArea ?? ""
|
||||
address.state = placemark.administrativeArea ?? ""
|
||||
address.postalCode = placemark.postalCode ?? ""
|
||||
address.country = placemark.country ?? ""
|
||||
address.isoCountryCode = placemark.isoCountryCode ?? ""
|
||||
|
||||
return (
|
||||
placemark.name,
|
||||
CNPostalAddressFormatter.string(
|
||||
from: address,
|
||||
style: .mailingAddress
|
||||
)
|
||||
)
|
||||
}
|
||||
catch {
|
||||
|
||||
return (nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreLocation
|
||||
import Combine
|
||||
import CoreStore
|
||||
import Foundation
|
||||
import MapKit
|
||||
@@ -14,7 +13,7 @@ import SwiftUI
|
||||
extension Modern.PlacemarksDemo {
|
||||
|
||||
// MARK: - Modern.PlacemarksDemo.MainView
|
||||
|
||||
|
||||
struct MainView: View {
|
||||
|
||||
/**
|
||||
@@ -38,7 +37,7 @@ extension Modern.PlacemarksDemo {
|
||||
- Important: `perform(synchronous:)` was used here for illustration purposes. In practice, `perform(asynchronous:completion:)` is the preferred transaction type as synchronous transactions are very likely to cause deadlocks.
|
||||
*/
|
||||
private func demoSynchronousTransaction() {
|
||||
|
||||
|
||||
_ = try? Modern.PlacemarksDemo.dataStack.perform(
|
||||
synchronous: { (transaction) in
|
||||
|
||||
@@ -68,41 +67,12 @@ extension Modern.PlacemarksDemo {
|
||||
print("Commit failed: \(error as Any)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
|
||||
@ObjectState(Modern.PlacemarksDemo.placePublisher)
|
||||
var place: ObjectSnapshot<Modern.PlacemarksDemo.Place>?
|
||||
|
||||
init() {
|
||||
|
||||
self.sinkCancellable = self.$place?.reactive.snapshot().sink(
|
||||
receiveCompletion: { _ in
|
||||
|
||||
// Deleted, do nothing
|
||||
},
|
||||
receiveValue: { [self] (snapshot) in
|
||||
|
||||
guard let snapshot = snapshot else {
|
||||
|
||||
return
|
||||
}
|
||||
self.geocoder.geocode(place: snapshot) { (title, subtitle) in
|
||||
|
||||
guard self.place == snapshot else {
|
||||
|
||||
return
|
||||
}
|
||||
self.demoUnsafeTransaction(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
for: snapshot
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
@@ -130,34 +100,41 @@ extension Modern.PlacemarksDemo {
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Placemarks")
|
||||
.navigationBarItems(
|
||||
trailing: Button("Random") {
|
||||
.task(id: self.place.map({ "\($0.$latitude),\($0.$longitude)" })) {
|
||||
|
||||
guard let place = self.place else {
|
||||
|
||||
return
|
||||
}
|
||||
let geocoded = await self.geocoder.geocode(place: place)
|
||||
guard self.place?.objectID() == place.objectID() else {
|
||||
|
||||
return
|
||||
}
|
||||
guard geocoded.title != nil || geocoded.subtitle != nil else {
|
||||
|
||||
return
|
||||
}
|
||||
self.demoUnsafeTransaction(
|
||||
title: geocoded.title,
|
||||
subtitle: geocoded.subtitle,
|
||||
for: place
|
||||
)
|
||||
}
|
||||
.navigationTitle("Placemarks")
|
||||
.toolbar {
|
||||
|
||||
Button("Random") {
|
||||
|
||||
self.demoSynchronousTransaction()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var sinkCancellable: AnyCancellable?
|
||||
private let geocoder = Modern.PlacemarksDemo.Geocoder()
|
||||
@State
|
||||
private var geocoder = Modern.PlacemarksDemo.Geocoder()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_PlacemarksDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Modern.PlacemarksDemo.MainView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,8 +11,8 @@ extension Modern {
|
||||
// MARK: - Modern.PlacemarksDemo
|
||||
|
||||
/**
|
||||
Sample usages for `CoreStoreObject` transactions
|
||||
*/
|
||||
Sample usages for `CoreStoreObject` transactions
|
||||
*/
|
||||
enum PlacemarksDemo {
|
||||
|
||||
// MARK: Internal
|
||||
@@ -20,6 +20,7 @@ extension Modern {
|
||||
/**
|
||||
⭐️ Sample 1: Setting up the `DataStack` and storage
|
||||
*/
|
||||
@MainActor
|
||||
static let dataStack: DataStack = {
|
||||
|
||||
let dataStack = DataStack(
|
||||
@@ -45,7 +46,8 @@ extension Modern {
|
||||
)
|
||||
return dataStack
|
||||
}()
|
||||
|
||||
|
||||
@MainActor
|
||||
static let placePublisher: ObjectPublisher<Modern.PlacemarksDemo.Place> = {
|
||||
|
||||
let dataStack = Modern.PlacemarksDemo.dataStack
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
extension Modern.PokedexDemo {
|
||||
|
||||
|
||||
// MARK: - Modern.PokedexDemo.MainView
|
||||
|
||||
|
||||
struct MainView<ListView: View>: View {
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(
|
||||
@@ -22,16 +21,16 @@ extension Modern.PokedexDemo {
|
||||
|
||||
self.listView = listView
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
||||
self.listView()
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
.edgesIgnoringSafeArea(.vertical)
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
.ignoresSafeArea(.container, edges: .vertical)
|
||||
|
||||
if self.pokedexEntries.isEmpty {
|
||||
|
||||
@@ -56,10 +55,10 @@ extension Modern.PokedexDemo {
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Pokedex")
|
||||
.navigationTitle("Pokedex")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@ListState(
|
||||
@@ -68,28 +67,10 @@ extension Modern.PokedexDemo {
|
||||
in: Modern.PokedexDemo.dataStack
|
||||
)
|
||||
private var pokedexEntries
|
||||
|
||||
@ObservedObject
|
||||
private var service: Modern.PokedexDemo.Service = .init()
|
||||
|
||||
@State
|
||||
private var service = Modern.PokedexDemo.Service()
|
||||
|
||||
private let listView: () -> ListView
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct _Demo_Modern_PokedexDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Modern.PokedexDemo.MainView(
|
||||
listView: Modern.PokedexDemo.UIKit.ListView.init
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,64 +2,48 @@
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreStore
|
||||
import UIKit
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
extension Modern.PokedexDemo {
|
||||
|
||||
|
||||
// MARK: - Modern.PokedexDemo.Service
|
||||
|
||||
final class Service: ObservableObject {
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class Service {
|
||||
|
||||
/**
|
||||
⭐️ Sample 1: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are tuples
|
||||
*/
|
||||
private static func importPokedexEntries(
|
||||
from output: URLSession.DataTaskPublisher.Output
|
||||
) -> Future<Void, Modern.PokedexDemo.Service.Error> {
|
||||
private static func importPokedexEntries(from data: Data) async throws {
|
||||
|
||||
return .init { promise in
|
||||
do {
|
||||
|
||||
Modern.PokedexDemo.dataStack.perform(
|
||||
asynchronous: { transaction -> Void in
|
||||
|
||||
let json: Dictionary<String, Any> = try self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: output.data, options: [])
|
||||
)
|
||||
let results: [Dictionary<String, Any>] = try self.parseJSON(
|
||||
json["results"]
|
||||
)
|
||||
_ = try transaction.importUniqueObjects(
|
||||
Into<Modern.PokedexDemo.PokedexEntry>(),
|
||||
sourceArray: results.enumerated().map { (index, json) in
|
||||
(index: index, json: json)
|
||||
}
|
||||
)
|
||||
},
|
||||
success: { result in
|
||||
|
||||
promise(.success(result))
|
||||
},
|
||||
failure: { error in
|
||||
|
||||
switch error {
|
||||
|
||||
case .userError(let error as Modern.PokedexDemo.Service.Error):
|
||||
promise(.failure(error))
|
||||
|
||||
case .userError(let error):
|
||||
promise(.failure(.otherError(error)))
|
||||
|
||||
case let error:
|
||||
promise(.failure(.saveError(error)))
|
||||
try await Modern.PokedexDemo.dataStack.async.perform { transaction -> Void in
|
||||
|
||||
let json: Dictionary<String, Any> = try self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: data, options: [])
|
||||
)
|
||||
let results: [Dictionary<String, Any>] = try self.parseJSON(
|
||||
json["results"]
|
||||
)
|
||||
_ = try transaction.importUniqueObjects(
|
||||
Into<Modern.PokedexDemo.PokedexEntry>(),
|
||||
sourceArray: results.enumerated().map { index, json in
|
||||
(index: index, json: json)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
||||
throw self.mapError(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,134 +51,93 @@ extension Modern.PokedexDemo {
|
||||
⭐️ Sample 2: Importing a single JSON data into an `ImportableUniqueObject` whose `ImportSource` is a JSON `Dictionary`
|
||||
*/
|
||||
private static func importSpecies(
|
||||
for details: ObjectSnapshot<Modern.PokedexDemo.Details>,
|
||||
from output: URLSession.DataTaskPublisher.Output
|
||||
) -> Future<ObjectSnapshot<Modern.PokedexDemo.Species>, Modern.PokedexDemo.Service.Error> {
|
||||
for detailsObjectID: NSManagedObjectID,
|
||||
from data: Data
|
||||
) async throws -> ObjectSnapshot<Modern.PokedexDemo.Species> {
|
||||
|
||||
return .init { promise in
|
||||
let speciesObjectID = try await Modern.PokedexDemo.dataStack.async.perform { transaction -> NSManagedObjectID in
|
||||
|
||||
Modern.PokedexDemo.dataStack.perform(
|
||||
asynchronous: { transaction -> Modern.PokedexDemo.Species in
|
||||
|
||||
let json: Dictionary<String, Any> = try self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: output.data, options: [])
|
||||
)
|
||||
guard
|
||||
let species = try transaction.importUniqueObject(
|
||||
Into<Modern.PokedexDemo.Species>(),
|
||||
source: json
|
||||
)
|
||||
else {
|
||||
|
||||
throw Modern.PokedexDemo.Service.Error.unexpected
|
||||
}
|
||||
details.asEditable(in: transaction)?.species = species
|
||||
return species
|
||||
},
|
||||
success: { species in
|
||||
|
||||
promise(.success(species.asSnapshot(in: Modern.PokedexDemo.dataStack)!))
|
||||
},
|
||||
failure: { error in
|
||||
|
||||
switch error {
|
||||
|
||||
case .userError(let error as Modern.PokedexDemo.Service.Error):
|
||||
promise(.failure(error))
|
||||
|
||||
case .userError(let error):
|
||||
promise(.failure(.otherError(error)))
|
||||
|
||||
case let error:
|
||||
promise(.failure(.saveError(error)))
|
||||
}
|
||||
}
|
||||
let json: Dictionary<String, Any> = try self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: data, options: [])
|
||||
)
|
||||
guard
|
||||
let species = try transaction.importUniqueObject(
|
||||
Into<Modern.PokedexDemo.Species>(),
|
||||
source: json
|
||||
)
|
||||
else {
|
||||
|
||||
throw Modern.PokedexDemo.Service.Error.unexpected
|
||||
}
|
||||
transaction
|
||||
.edit(Into<Modern.PokedexDemo.Details>(), detailsObjectID)?
|
||||
.species = species
|
||||
return species.objectID()
|
||||
}
|
||||
guard
|
||||
let species: Modern.PokedexDemo.Species = Modern.PokedexDemo.dataStack.fetchExisting(speciesObjectID),
|
||||
let snapshot = species.asSnapshot()
|
||||
else {
|
||||
|
||||
throw Modern.PokedexDemo.Service.Error.unexpected
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
/**
|
||||
⭐️ Sample 3: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are JSON `Dictionary`s
|
||||
*/
|
||||
private static func importForms(
|
||||
for details: ObjectSnapshot<Modern.PokedexDemo.Details>,
|
||||
from outputs: [URLSession.DataTaskPublisher.Output]
|
||||
) -> Future<Void, Modern.PokedexDemo.Service.Error> {
|
||||
for detailsObjectID: NSManagedObjectID,
|
||||
from dataArray: [Data]
|
||||
) async throws {
|
||||
|
||||
return .init { promise in
|
||||
do {
|
||||
|
||||
Modern.PokedexDemo.dataStack.perform(
|
||||
asynchronous: { transaction -> Void in
|
||||
|
||||
let forms = try transaction.importUniqueObjects(
|
||||
Into<Modern.PokedexDemo.Form>(),
|
||||
sourceArray: outputs.map { output in
|
||||
|
||||
return try self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: output.data, options: [])
|
||||
)
|
||||
}
|
||||
)
|
||||
guard !forms.isEmpty else {
|
||||
try await Modern.PokedexDemo.dataStack.async.perform { transaction -> Void in
|
||||
|
||||
let forms = try transaction.importUniqueObjects(
|
||||
Into<Modern.PokedexDemo.Form>(),
|
||||
sourceArray: dataArray.map { data in
|
||||
|
||||
throw Modern.PokedexDemo.Service.Error.unexpected
|
||||
try self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: data, options: [])
|
||||
) as [String: Any]
|
||||
}
|
||||
details.asEditable(in: transaction)?.forms = forms
|
||||
},
|
||||
success: {
|
||||
)
|
||||
guard !forms.isEmpty else {
|
||||
|
||||
promise(.success(()))
|
||||
},
|
||||
failure: { error in
|
||||
|
||||
switch error {
|
||||
|
||||
case .userError(let error as Modern.PokedexDemo.Service.Error):
|
||||
promise(.failure(error))
|
||||
|
||||
case .userError(let error):
|
||||
promise(.failure(.otherError(error)))
|
||||
|
||||
case let error:
|
||||
promise(.failure(.saveError(error)))
|
||||
}
|
||||
throw Modern.PokedexDemo.Service.Error.unexpected
|
||||
}
|
||||
)
|
||||
transaction
|
||||
.edit(Into<Modern.PokedexDemo.Details>(), detailsObjectID)?
|
||||
.forms = forms
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
||||
throw self.mapError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
private(set) var isLoading: Bool = false {
|
||||
|
||||
willSet {
|
||||
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var lastError: (error: Modern.PokedexDemo.Service.Error, retry: () -> Void)? {
|
||||
|
||||
willSet {
|
||||
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var isLoading: Bool = false
|
||||
|
||||
init() {}
|
||||
|
||||
static func parseJSON<Output>(
|
||||
|
||||
static nonisolated func parseJSON<Output>(
|
||||
_ json: Any?,
|
||||
file: StaticString = #file,
|
||||
line: Int = #line
|
||||
) throws -> Output {
|
||||
|
||||
|
||||
switch json {
|
||||
|
||||
|
||||
case let json as Output:
|
||||
return json
|
||||
|
||||
|
||||
case let any:
|
||||
throw Modern.PokedexDemo.Service.Error.parseError(
|
||||
expected: Output.self,
|
||||
@@ -203,20 +146,20 @@ extension Modern.PokedexDemo {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static func parseJSON<JSONType, Output>(
|
||||
|
||||
static nonisolated func parseJSON<JSONType, Output>(
|
||||
_ json: Any?,
|
||||
transformer: (JSONType) throws -> Output?,
|
||||
file: StaticString = #file,
|
||||
line: Int = #line
|
||||
) throws -> Output {
|
||||
|
||||
|
||||
switch json {
|
||||
|
||||
|
||||
case let json as JSONType:
|
||||
let transformed = try transformer(json)
|
||||
if let json = transformed {
|
||||
|
||||
|
||||
return json
|
||||
}
|
||||
throw Modern.PokedexDemo.Service.Error.parseError(
|
||||
@@ -224,7 +167,7 @@ extension Modern.PokedexDemo {
|
||||
actual: type(of: transformed),
|
||||
file: "\(file):\(line)"
|
||||
)
|
||||
|
||||
|
||||
case let any:
|
||||
throw Modern.PokedexDemo.Service.Error.parseError(
|
||||
expected: Output.self,
|
||||
@@ -233,153 +176,235 @@ extension Modern.PokedexDemo {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func fetchPokedexEntries() {
|
||||
|
||||
self.cancellable["pokedexEntries"] = self.pokedexEntries
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents(
|
||||
receiveSubscription: { [weak self] _ in
|
||||
|
||||
guard let self = self else {
|
||||
|
||||
return
|
||||
}
|
||||
self.lastError = nil
|
||||
self.isLoading = true
|
||||
}
|
||||
)
|
||||
.sink(
|
||||
receiveCompletion: { [weak self] completion in
|
||||
|
||||
guard let self = self else {
|
||||
|
||||
return
|
||||
}
|
||||
self.isLoading = false
|
||||
switch completion {
|
||||
|
||||
case .finished:
|
||||
self.lastError = nil
|
||||
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
self.lastError = (
|
||||
error: error,
|
||||
retry: { [weak self] in
|
||||
|
||||
self?.fetchPokedexEntries()
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
receiveValue: {}
|
||||
)
|
||||
|
||||
self.pokedexEntriesTask?.cancel()
|
||||
self.pokedexEntriesTask = Task { [weak self] in
|
||||
|
||||
await self?.runFetchPokedexEntries()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchDetails(for pokedexEntry: ObjectSnapshot<Modern.PokedexDemo.PokedexEntry>) {
|
||||
|
||||
self.fetchSpeciesIfNeeded(for: pokedexEntry)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var cancellable: Dictionary<String, AnyCancellable> = [:]
|
||||
|
||||
private lazy var pokedexEntries: AnyPublisher<Void, Modern.PokedexDemo.Service.Error> = URLSession.shared
|
||||
.dataTaskPublisher(
|
||||
for: URL(string: "https://pokeapi.co/api/v2/pokemon?limit=10000&offset=0")!
|
||||
)
|
||||
.mapError({ .networkError($0) })
|
||||
.flatMap(Self.importPokedexEntries(from:))
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
private func fetchSpeciesIfNeeded(for pokedexEntry: ObjectSnapshot<Modern.PokedexDemo.PokedexEntry>) {
|
||||
|
||||
guard let details = pokedexEntry.$details?.snapshot else {
|
||||
|
||||
return
|
||||
}
|
||||
if let species = details.$species?.snapshot {
|
||||
|
||||
self.fetchFormsIfNeeded(for: species)
|
||||
self.fetchFormsIfNeeded(
|
||||
key: species.$id,
|
||||
detailsObjectID: details.objectID(),
|
||||
species: species
|
||||
)
|
||||
return
|
||||
}
|
||||
self.cancellable["species.\(pokedexEntry.$id)"] = URLSession.shared
|
||||
.dataTaskPublisher(for: pokedexEntry.$speciesURL)
|
||||
.mapError({ .networkError($0) })
|
||||
.flatMap({ Self.importSpecies(for: details, from: $0) })
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
|
||||
switch completion {
|
||||
|
||||
case .finished:
|
||||
break
|
||||
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
}
|
||||
},
|
||||
receiveValue: { species in
|
||||
|
||||
self.fetchFormsIfNeeded(for: species)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func fetchFormsIfNeeded(for species: ObjectSnapshot<Modern.PokedexDemo.Species>) {
|
||||
|
||||
guard
|
||||
let details = species.$details?.snapshot,
|
||||
details.$forms.isEmpty
|
||||
else {
|
||||
let key = pokedexEntry.$id
|
||||
guard self.detailTasks[key] == nil else {
|
||||
|
||||
return
|
||||
}
|
||||
self.cancellable["forms.\(species.$id)"] = species
|
||||
.$formsURLs
|
||||
.map(
|
||||
{
|
||||
URLSession.shared
|
||||
.dataTaskPublisher(for: $0)
|
||||
.mapError({ Modern.PokedexDemo.Service.Error.networkError($0) })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
)
|
||||
.reduce(
|
||||
into: Just<[URLSession.DataTaskPublisher.Output]>([])
|
||||
.setFailureType(to: Modern.PokedexDemo.Service.Error.self)
|
||||
.eraseToAnyPublisher(),
|
||||
{ (result, publisher) in
|
||||
result = result
|
||||
.zip(publisher, { $0 + [$1] })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
)
|
||||
.flatMap({ Self.importForms(for: details, from: $0) })
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
|
||||
switch completion {
|
||||
|
||||
case .finished:
|
||||
break
|
||||
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
}
|
||||
},
|
||||
receiveValue: { _ in }
|
||||
let speciesURL = pokedexEntry.$speciesURL
|
||||
let detailsObjectID = details.objectID()
|
||||
self.detailTasks[key] = Task { [weak self] in
|
||||
|
||||
guard let self else {
|
||||
|
||||
return
|
||||
}
|
||||
defer {
|
||||
|
||||
self.detailTasks.removeValue(forKey: key)
|
||||
}
|
||||
await self.fetchSpecies(
|
||||
key: key,
|
||||
detailsObjectID: detailsObjectID,
|
||||
speciesURL: speciesURL
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@ObservationIgnored
|
||||
private static let pokedexURL = URL(
|
||||
string: "https://pokeapi.co/api/v2/pokemon?limit=10000&offset=0"
|
||||
)!
|
||||
|
||||
@ObservationIgnored
|
||||
private var pokedexEntriesTask: Task<Void, Never>?
|
||||
|
||||
@ObservationIgnored
|
||||
private var detailTasks: [String: Task<Void, Never>] = [:]
|
||||
|
||||
private static func mapError(_ error: CoreStoreError) -> Modern.PokedexDemo.Service.Error {
|
||||
|
||||
switch error {
|
||||
case .userError(let error as Modern.PokedexDemo.Service.Error):
|
||||
return error
|
||||
|
||||
case .userError(let error):
|
||||
return .otherError(error)
|
||||
|
||||
case let error:
|
||||
return .saveError(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func runFetchPokedexEntries() async {
|
||||
|
||||
self.isLoading = true
|
||||
defer {
|
||||
|
||||
self.isLoading = false
|
||||
self.pokedexEntriesTask = nil
|
||||
}
|
||||
|
||||
do {
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(from: Self.pokedexURL)
|
||||
try Task.checkCancellation()
|
||||
try await Self.importPokedexEntries(from: data)
|
||||
}
|
||||
catch is CancellationError {
|
||||
|
||||
return
|
||||
}
|
||||
catch let error as Modern.PokedexDemo.Service.Error {
|
||||
|
||||
print(error)
|
||||
}
|
||||
catch let error as URLError {
|
||||
|
||||
print(Modern.PokedexDemo.Service.Error.networkError(error))
|
||||
}
|
||||
catch {
|
||||
|
||||
print(Modern.PokedexDemo.Service.Error.otherError(error))
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchSpecies(
|
||||
key: String,
|
||||
detailsObjectID: NSManagedObjectID,
|
||||
speciesURL: URL
|
||||
) async {
|
||||
|
||||
do {
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(from: speciesURL)
|
||||
try Task.checkCancellation()
|
||||
|
||||
let species = try await Self.importSpecies(
|
||||
for: detailsObjectID,
|
||||
from: data
|
||||
)
|
||||
guard species.$details?.snapshot?.$forms.isEmpty == true else {
|
||||
|
||||
return
|
||||
}
|
||||
await self.fetchForms(
|
||||
detailsObjectID: detailsObjectID,
|
||||
formsURLs: species.$formsURLs
|
||||
)
|
||||
}
|
||||
catch is CancellationError {
|
||||
|
||||
return
|
||||
}
|
||||
catch let error as Modern.PokedexDemo.Service.Error {
|
||||
|
||||
print(error)
|
||||
}
|
||||
catch let error as URLError {
|
||||
|
||||
print(Modern.PokedexDemo.Service.Error.networkError(error))
|
||||
}
|
||||
catch {
|
||||
|
||||
print(Modern.PokedexDemo.Service.Error.otherError(error))
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchFormsIfNeeded(
|
||||
key: String,
|
||||
detailsObjectID: NSManagedObjectID,
|
||||
species: ObjectSnapshot<Modern.PokedexDemo.Species>
|
||||
) {
|
||||
|
||||
guard species.$details?.snapshot?.$forms.isEmpty == true else {
|
||||
|
||||
return
|
||||
}
|
||||
guard self.detailTasks[key] == nil else {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let formsURLs = species.$formsURLs
|
||||
self.detailTasks[key] = Task { [weak self] in
|
||||
|
||||
guard let self else {
|
||||
|
||||
return
|
||||
}
|
||||
defer {
|
||||
|
||||
self.detailTasks.removeValue(forKey: key)
|
||||
}
|
||||
await self.fetchForms(
|
||||
detailsObjectID: detailsObjectID,
|
||||
formsURLs: formsURLs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchForms(
|
||||
detailsObjectID: NSManagedObjectID,
|
||||
formsURLs: [URL]
|
||||
) async {
|
||||
|
||||
do {
|
||||
|
||||
var dataArray: [Data] = []
|
||||
dataArray.reserveCapacity(formsURLs.count)
|
||||
|
||||
for url in formsURLs {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
try Task.checkCancellation()
|
||||
dataArray.append(data)
|
||||
}
|
||||
try await Self.importForms(
|
||||
for: detailsObjectID,
|
||||
from: dataArray
|
||||
)
|
||||
}
|
||||
catch is CancellationError {
|
||||
|
||||
return
|
||||
}
|
||||
catch let error as Modern.PokedexDemo.Service.Error {
|
||||
|
||||
print(error)
|
||||
}
|
||||
catch let error as URLError {
|
||||
|
||||
print(Modern.PokedexDemo.Service.Error.networkError(error))
|
||||
}
|
||||
catch {
|
||||
|
||||
print(Modern.PokedexDemo.Service.Error.otherError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Modern.PokedexDemo.Service.Error
|
||||
|
||||
|
||||
enum Error: Swift.Error {
|
||||
|
||||
|
||||
case networkError(URLError)
|
||||
case parseError(expected: Any.Type, actual: Any.Type, file: String)
|
||||
case saveError(CoreStoreError)
|
||||
|
||||
@@ -12,12 +12,11 @@ extension Modern.PokedexDemo.UIKit {
|
||||
// MARK: - Modern.PokedexDemo.ListView
|
||||
|
||||
struct ListView: UIViewControllerRepresentable {
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
|
||||
init() {
|
||||
|
||||
self.service = Modern.PokedexDemo.Service.init()
|
||||
self._service = State(initialValue: Modern.PokedexDemo.Service())
|
||||
self.listPublisher = Modern.PokedexDemo.dataStack
|
||||
.publishList(
|
||||
From<Modern.PokedexDemo.PokedexEntry>()
|
||||
@@ -41,30 +40,20 @@ extension Modern.PokedexDemo.UIKit {
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {}
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@ObservedObject
|
||||
|
||||
@State
|
||||
private var service: Modern.PokedexDemo.Service
|
||||
|
||||
|
||||
private let listPublisher: ListPublisher<Modern.PokedexDemo.PokedexEntry>
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_PokedexDemo_UIKit_ListView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let service = Modern.PokedexDemo.Service()
|
||||
service.fetchPokedexEntries()
|
||||
|
||||
return Modern.PokedexDemo.UIKit.ListView()
|
||||
}
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
Modern.PokedexDemo.UIKit.ListView()
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,12 +11,13 @@ extension Modern {
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
/**
|
||||
Sample usages for importing external data into `CoreStoreObject` attributes
|
||||
*/
|
||||
Sample usages for importing external data into `CoreStoreObject` attributes
|
||||
*/
|
||||
enum PokedexDemo {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
@MainActor
|
||||
static let dataStack: DataStack = {
|
||||
|
||||
let dataStack = DataStack(
|
||||
@@ -49,6 +50,7 @@ extension Modern {
|
||||
return dataStack
|
||||
}()
|
||||
|
||||
@MainActor
|
||||
static let pokedexEntries: ListPublisher<Modern.PokedexDemo.PokedexEntry> = Modern.PokedexDemo.dataStack.publishList(
|
||||
From<Modern.PokedexDemo.PokedexEntry>()
|
||||
.orderBy(.ascending(\.$index))
|
||||
|
||||
@@ -15,18 +15,22 @@ extension Modern.TimeZonesDemo {
|
||||
// MARK: Internal
|
||||
|
||||
init(title: String, subtitle: String) {
|
||||
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Text(self.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(self.subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
@@ -40,19 +44,3 @@ extension Modern.TimeZonesDemo {
|
||||
fileprivate let subtitle: String
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_TimeZonesDemo_ItemView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
Modern.TimeZonesDemo.ItemView(
|
||||
title: "Item Title",
|
||||
subtitle: "A subtitle caption for this item"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
extension Modern.TimeZonesDemo {
|
||||
|
||||
// MARK: - Modern.TimeZonesDemo.ListView
|
||||
|
||||
|
||||
struct ListView: View {
|
||||
|
||||
// MARK: Internal
|
||||
@@ -53,15 +53,18 @@ extension Modern.TimeZonesDemo {
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
|
||||
List {
|
||||
|
||||
ForEach(self.values, id: \.title) { item in
|
||||
|
||||
Modern.TimeZonesDemo.ItemView(
|
||||
title: item.title,
|
||||
subtitle: item.subtitle
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(self.title)
|
||||
.navigationTitle(self.title)
|
||||
}
|
||||
|
||||
|
||||
@@ -71,24 +74,3 @@ extension Modern.TimeZonesDemo {
|
||||
private let values: [(title: String, subtitle: String)]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_TimeZonesDemo_ListView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Modern.TimeZonesDemo.ListView(
|
||||
title: "Title",
|
||||
objects: try! Modern.TimeZonesDemo.dataStack.fetchAll(
|
||||
From<Modern.TimeZonesDemo.TimeZone>()
|
||||
.orderBy(.ascending(\.$name))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
extension Modern.TimeZonesDemo {
|
||||
|
||||
// MARK: - Modern.TimeZonesDemo.MainView
|
||||
|
||||
|
||||
struct MainView: View {
|
||||
|
||||
/**
|
||||
@@ -61,11 +61,11 @@ extension Modern.TimeZonesDemo {
|
||||
return try! Modern.TimeZonesDemo.dataStack.fetchAll(
|
||||
From<Modern.TimeZonesDemo.TimeZone>()
|
||||
.where((-secondsIn3Hours ... secondsIn3Hours) ~= \.$secondsFromGMT)
|
||||
/// equivalent to:
|
||||
/// ```
|
||||
/// .where(\.$secondsFromGMT >= -secondsIn3Hours
|
||||
/// && \.$secondsFromGMT <= secondsIn3Hours)
|
||||
/// ```
|
||||
/// equivalent to:
|
||||
/// ```
|
||||
/// .where(\.$secondsFromGMT >= -secondsIn3Hours
|
||||
/// && \.$secondsFromGMT <= secondsIn3Hours)
|
||||
/// ```
|
||||
.orderBy(.ascending(\.$secondsFromGMT))
|
||||
)
|
||||
}
|
||||
@@ -137,36 +137,54 @@ extension Modern.TimeZonesDemo {
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
|
||||
List {
|
||||
Section(header: Text("Fetching objects")) {
|
||||
|
||||
Section("Fetching objects") {
|
||||
|
||||
ForEach(self.fetchingItems, id: \.title) { item in
|
||||
Menu.ItemView(
|
||||
title: item.title,
|
||||
|
||||
NavigationLink(
|
||||
destination: {
|
||||
|
||||
Modern.TimeZonesDemo.ListView(
|
||||
title: item.title,
|
||||
objects: item.objects()
|
||||
)
|
||||
},
|
||||
label: {
|
||||
|
||||
Menu.ItemView(
|
||||
title: item.title
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Section(header: Text("Querying raw values")) {
|
||||
Section("Querying raw values") {
|
||||
|
||||
ForEach(self.queryingItems, id: \.title) { item in
|
||||
Menu.ItemView(
|
||||
title: item.title,
|
||||
|
||||
NavigationLink(
|
||||
destination: {
|
||||
|
||||
Modern.TimeZonesDemo.ListView(
|
||||
title: item.title,
|
||||
value: item.value()
|
||||
)
|
||||
},
|
||||
label: {
|
||||
|
||||
Menu.ItemView(
|
||||
title: item.title
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
.navigationBarTitle("Time Zones")
|
||||
.listStyle(.grouped)
|
||||
.navigationTitle("Time Zones")
|
||||
}
|
||||
|
||||
|
||||
@@ -217,18 +235,3 @@ extension Modern.TimeZonesDemo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_TimeZonesDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Modern.TimeZonesDemo.MainView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,8 +11,8 @@ extension Modern {
|
||||
// MARK: - Modern.TimeZonesDemo
|
||||
|
||||
/**
|
||||
Sample usages for creating Fetch and Query clauses for `CoreStoreObject`s
|
||||
*/
|
||||
Sample usages for creating Fetch and Query clauses for `CoreStoreObject`s
|
||||
*/
|
||||
enum TimeZonesDemo {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
Reference in New Issue
Block a user