This commit is contained in:
John Estropia
2026-07-15 11:50:43 +09:00
parent 7db1cfecfb
commit 890e150b95
135 changed files with 2895 additions and 2526 deletions
@@ -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
@@ -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()
}
)
}
@@ -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>()
)
@@ -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>()
)
@@ -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
@@ -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
@@ -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