Xcode 14, iOS 16 SDK (min iOS 13)

This commit is contained in:
John Estropia
2022-06-19 17:56:42 +09:00
parent 3317867a2f
commit d1f83badef
121 changed files with 217 additions and 235 deletions

View File

@@ -0,0 +1,33 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import UIKit
// MARK: - AppDelegate
@UIApplicationMain
@objc final class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: UIApplicationDelegate
@objc dynamic func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
return true
}
@objc dynamic func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
return UISceneConfiguration(
name: "Default Configuration",
sessionRole: connectingSceneSession.role
)
}
}

View File

@@ -0,0 +1,10 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
// MARK: - Advanced
/**
Sample application of complex use cases
*/
enum Advanced {}

View File

@@ -0,0 +1,30 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import Foundation
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
typealias CreatureType = Advanced_EvolutionDemo_CreatureType
}
// MARK: - Advanced.EvolutionDemo.CreatureType
protocol Advanced_EvolutionDemo_CreatureType: DynamicObject, CustomStringConvertible {
var dnaCode: Int64 { get set }
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource
static func count(in transaction: BaseDataTransaction) throws -> Int
static func create(in transaction: BaseDataTransaction) -> Self
func mutate(in transaction: BaseDataTransaction)
}

View File

@@ -0,0 +1,168 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import Combine
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.CreaturesDataSource
/**
A type-erasing adapter to support different `ListPublisher` types
*/
final class CreaturesDataSource: ObservableObject {
// MARK: Internal
init<T: NSManagedObject & 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()
}
}
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 {
return self.numberOfItems()
}
func creatureDescription(at index: Int) -> String? {
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 let numberOfItems: () -> Int
private let itemDescriptionAtIndex: (Int) -> String?
private let mutateItemAtIndex: (Int) -> Void
private let addItems: (Int) -> Void
private let deleteAllItems: () -> Void
}
}

View File

@@ -0,0 +1,99 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - AdvancedEvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - GeologicalPeriod
enum GeologicalPeriod: RawRepresentable, CaseIterable, Hashable, CustomStringConvertible {
// MARK: Internal
case ageOfInvertebrates
case ageOfFishes
case ageOfReptiles
case ageOfMammals
var version: ModelVersion {
return self.rawValue
}
var creatureType: Advanced.EvolutionDemo.CreatureType.Type {
switch self {
case .ageOfInvertebrates:
return Advanced.EvolutionDemo.V1.Creature.self
case .ageOfFishes:
return Advanced.EvolutionDemo.V2.Creature.self
case .ageOfReptiles:
return Advanced.EvolutionDemo.V3.Creature.self
case .ageOfMammals:
return Advanced.EvolutionDemo.V4.Creature.self
}
}
// MARK: CustomStringConvertible
var description: String {
switch self {
case .ageOfInvertebrates:
return "Invertebrates"
case .ageOfFishes:
return "Fishes"
case .ageOfReptiles:
return "Reptiles"
case .ageOfMammals:
return "Mammals"
}
}
// MARK: RawRepresentable
typealias RawValue = ModelVersion
var rawValue: ModelVersion {
switch self {
case .ageOfInvertebrates:
return Advanced.EvolutionDemo.V1.name
case .ageOfFishes:
return Advanced.EvolutionDemo.V2.name
case .ageOfReptiles:
return Advanced.EvolutionDemo.V3.name
case .ageOfMammals:
return Advanced.EvolutionDemo.V4.name
}
}
init?(rawValue: ModelVersion) {
switch rawValue {
case Advanced.EvolutionDemo.V1.name:
self = .ageOfInvertebrates
case Advanced.EvolutionDemo.V2.name:
self = .ageOfFishes
case Advanced.EvolutionDemo.V3.name:
self = .ageOfReptiles
case Advanced.EvolutionDemo.V4.name:
self = .ageOfMammals
default:
return nil
}
}
}
}

View File

@@ -0,0 +1,75 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
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())
}
.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

View File

@@ -0,0 +1,99 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
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)
}
}
)
return Advanced.EvolutionDemo.ListView(
period: .ageOfMammals,
dataStack: dataStack,
dataSource: Advanced.EvolutionDemo.V4.Creature.dataSource(in: dataStack)
)
}
}
#endif

View File

@@ -0,0 +1,78 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import SwiftUI
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.MainView
struct MainView: View {
// MARK: View
var body: some View {
let migrator = self.migrator
let listView: AnyView
if let current = migrator.current {
listView = AnyView(
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)
}
.navigationBarTitle("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

View File

@@ -0,0 +1,260 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import Foundation
import Combine
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.Migrator
final class Migrator: ObservableObject {
/**
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.
*/
private func createDataStack(
exactCurrentModelVersion: ModelVersion?,
migrationChain: MigrationChain
) -> DataStack {
let xcodeV1ToV2ModelSchema = XcodeDataModelSchema.from(
modelName: "Advanced.EvolutionDemo.V1",
bundle: Bundle(for: Advanced.EvolutionDemo.V1.Creature.self)
)
return DataStack(
schemaHistory: SchemaHistory(
allSchema: xcodeV1ToV2ModelSchema.allSchema
+ [
CoreStoreSchema(
modelVersion: Advanced.EvolutionDemo.V3.name,
entities: [
Entity<Advanced.EvolutionDemo.V3.Creature>("Creature")
]
),
CoreStoreSchema(
modelVersion: Advanced.EvolutionDemo.V4.name,
entities: [
Entity<Advanced.EvolutionDemo.V4.Creature>("Creature")
]
)
],
migrationChain: migrationChain,
exactCurrentModelVersion: exactCurrentModelVersion
)
)
}
/**
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,
Advanced.EvolutionDemo.V4.FromV3.mapping
]
let downgradeMappings: [SchemaMappingProvider] = [
Advanced.EvolutionDemo.V3.FromV4.mapping,
Advanced.EvolutionDemo.V2.FromV3.mapping,
Advanced.EvolutionDemo.V1.FromV2.mapping,
]
return SQLiteStore(
fileName: "Advanced.EvolutionDemo.sqlite",
configuration: nil,
migrationMappingProviders: upgradeMappings + downgradeMappings,
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 })
// Since we are only interested in finding current version, we'll assume an upgrading `MigrationChain`
let dataStack = self.createDataStack(
exactCurrentModelVersion: nil,
migrationChain: MigrationChain(allVersions)
)
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!
}
// 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()
}
// MARK: Private
private func synchronizeCurrentVersion() {
guard
let currentPeriod = Advanced.EvolutionDemo.GeologicalPeriod(rawValue: self.findCurrentVersion())
else {
self.selectModelVersion(self.currentPeriod)
return
}
self.selectModelVersion(currentPeriod)
}
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 })
let currentVersionIndex = upgradeMigrationChain.firstIndex(of: currentVersion)!
let newVersionIndex = upgradeMigrationChain.firstIndex(of: newVersion)!
migrationChain = MigrationChain(
currentVersionIndex > newVersionIndex
? 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 {
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)
}
else {
completion()
}
}
)
}
private func spawnCreatures(
in dataStack: DataStack,
period: Advanced.EvolutionDemo.GeologicalPeriod,
completion: @escaping () -> Void
) {
dataStack.perform(
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() }
)
}
// MARK: - VersionMetadata
private struct VersionMetadata {
let label: String
let entityType: Advanced.EvolutionDemo.CreatureType.Type
let schemaHistory: SchemaHistory
}
}
}

View File

@@ -0,0 +1,126 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import SwiftUI
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.ProgressView
struct ProgressView: View {
// MARK: Internal
init(progress: Progress?) {
self.progressObserver = .init(progress)
}
// MARK: View
var body: some View {
guard self.progressObserver.isMigrating else {
return AnyView(
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()
)
}
// MARK: FilePrivate
@ObservedObject
private var progressObserver: ProgressObserver
// MARK: - ProgressObserver
fileprivate final class ProgressObserver: ObservableObject {
private(set) var fractionCompleted: CGFloat = 0
private(set) var localizedDescription: String = ""
private(set) var localizedAdditionalDescription: String = ""
var isMigrating: Bool {
return 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 ?? ""
}
}
// MARK: Private
private let progress: Progress?
}
}
}
#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

View File

@@ -0,0 +1,63 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import UIKit
import CoreStore
// MARK: - Advanced.EvolutionDemo.V1.Creature
@objc(Advanced_EvolutionDemo_V1_Creature)
final class Advanced_EvolutionDemo_V1_Creature: NSManagedObject, Advanced.EvolutionDemo.CreatureType {
@NSManaged
dynamic var dnaCode: Int64
@NSManaged
dynamic var numberOfFlagella: Int32
// MARK: CustomStringConvertible
override var description: String {
return """
dnaCode: \(self.dnaCode)
numberOfFlagella: \(self.numberOfFlagella)
"""
}
// MARK: Advanced.EvolutionDemo.CreatureType
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
return .init(
listPublisher: dataStack.publishList(
From<Advanced.EvolutionDemo.V1.Creature>()
.orderBy(.descending(\.dnaCode))
),
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>()
)
}
func mutate(in transaction: BaseDataTransaction) {
self.numberOfFlagella = .random(in: 1...200)
}
}

View File

@@ -0,0 +1,27 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Advanced.EvolutionDemo.V1
extension Advanced.EvolutionDemo.V1 {
// MARK: - Advanced.EvolutionDemo.V1.FromV2
enum FromV2 {
// MARK: Internal
static var mapping: XcodeSchemaMappingProvider {
return XcodeSchemaMappingProvider(
from: Advanced.EvolutionDemo.V2.name,
to: Advanced.EvolutionDemo.V1.name,
mappingModelBundle: Bundle(for: Advanced.EvolutionDemo.V1.Creature.self)
)
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.V1
/**
Namespace for V1 models (`Advanced.EvolutionDemo.GeologicalPeriod.ageOfInvertebrates`)
*/
enum V1 {
// MARK: Internal
static let name: ModelVersion = "Advanced.EvolutionDemo.V1"
typealias Creature = Advanced_EvolutionDemo_V1_Creature
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Advanced.EvolutionDemo.V1.xcdatamodel</string>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19F101" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Creature" representedClassName="Advanced_EvolutionDemo_V1_Creature" syncable="YES">
<attribute name="dnaCode" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numberOfFlagella" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
</entity>
<elements>
<element name="Creature" positionX="-27" positionY="18" width="128" height="73"/>
</elements>
</model>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19F101" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Creature" representedClassName="Advanced_EvolutionDemo_V2_Creature" syncable="YES">
<attribute name="dnaCode" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hasHead" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="hasTail" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="hasVertebrae" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="numberOfFlippers" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
</entity>
<elements>
<element name="Creature" positionX="-9" positionY="36" width="128" height="118"/>
</elements>
</model>

View File

@@ -0,0 +1,78 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import UIKit
import CoreStore
// MARK: - Advanced.EvolutionDemo.V2.Creature
@objc(Advanced_EvolutionDemo_V2_Creature)
final class Advanced_EvolutionDemo_V2_Creature: NSManagedObject, Advanced.EvolutionDemo.CreatureType {
@NSManaged
dynamic var dnaCode: Int64
@NSManaged
dynamic var numberOfFlippers: Int32
@NSManaged
dynamic var hasVertebrae: Bool
@NSManaged
dynamic var hasHead: Bool
@NSManaged
dynamic var hasTail: Bool
// MARK: CustomStringConvertible
override var description: String {
return """
dnaCode: \(self.dnaCode)
numberOfFlippers: \(self.numberOfFlippers)
hasVertebrae: \(self.hasVertebrae)
hasHead: \(self.hasHead)
hasTail: \(self.hasTail)
"""
}
// MARK: Advanced.EvolutionDemo.CreatureType
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
return .init(
listPublisher: dataStack.publishList(
From<Advanced.EvolutionDemo.V2.Creature>()
.orderBy(.descending(\.dnaCode))
),
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>()
)
}
func mutate(in transaction: BaseDataTransaction) {
self.numberOfFlippers = .random(in: 1...4) * 2
self.hasVertebrae = .random()
self.hasHead = true
self.hasTail = .random()
}
}

View File

@@ -0,0 +1,27 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Advanced.EvolutionDemo.V2
extension Advanced.EvolutionDemo.V2 {
// MARK: - Advanced.EvolutionDemo.V2.FromV1
enum FromV1 {
// MARK: Internal
static var mapping: XcodeSchemaMappingProvider {
return XcodeSchemaMappingProvider(
from: Advanced.EvolutionDemo.V1.name,
to: Advanced.EvolutionDemo.V2.name,
mappingModelBundle: Bundle(for: Advanced.EvolutionDemo.V1.Creature.self)
)
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,40 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreData
import CoreStore
// MARK: - Advanced.EvolutionDemo.V2.FromV1MigrationPolicy
@objc(Advanced_EvolutionDemo_V2_FromV1MigrationPolicy)
final class Advanced_EvolutionDemo_V2_FromV1MigrationPolicy: NSEntityMigrationPolicy {
// MARK: NSEntityMigrationPolicy
override func createDestinationInstances(
forSource sInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
try super.createDestinationInstances(
forSource: sInstance,
in: mapping,
manager: manager
)
for dInstance in manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]) {
dInstance.setValue(
Bool.random(),
forKey: #keyPath(Advanced.EvolutionDemo.V2.Creature.hasVertebrae)
)
dInstance.setValue(
Bool.random(),
forKey: #keyPath(Advanced.EvolutionDemo.V2.Creature.hasTail)
)
}
}
}

View File

@@ -0,0 +1,41 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Advanced.EvolutionDemo.V2
extension Advanced.EvolutionDemo.V2 {
// MARK: - Advanced.EvolutionDemo.V2.FromV3
enum FromV3 {
// MARK: Internal
static var mapping: CustomSchemaMappingProvider {
return CustomSchemaMappingProvider(
from: Advanced.EvolutionDemo.V3.name,
to: Advanced.EvolutionDemo.V2.name,
entityMappings: [
.transformEntity(
sourceEntity: "Creature",
destinationEntity: "Creature",
transformer: { (source, createDestination) in
let destination = createDestination()
destination["dnaCode"] = source["dnaCode"]
destination["numberOfFlippers"] = source["numberOfLimbs"]
destination["hasVertebrae"] = source["hasVertebrae"]
destination["hasHead"] = source["hasHead"]
destination["hasTail"] = source["hasTail"]
}
)
]
)
}
}
}

View File

@@ -0,0 +1,27 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.V2
/**
Namespace for V2 models (`Advanced.EvolutionDemo.GeologicalPeriod.ageOfFishes`)
*/
enum V2 {
// MARK: Internal
static let name: ModelVersion = "Advanced.EvolutionDemo.V2"
typealias Creature = Advanced_EvolutionDemo_V2_Creature
typealias FromV1MigrationPolicy = Advanced_EvolutionDemo_V2_FromV1MigrationPolicy
}
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19F101" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Creature" representedClassName="Advanced_EvolutionDemo_V2_Creature" syncable="YES">
<attribute name="dnaCode" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hasHead" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="hasTail" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="hasVertebrae" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="numberOfFlippers" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
</entity>
<elements>
<element name="Creature" positionX="-45" positionY="0" width="128" height="118"/>
</elements>
</model>

View File

@@ -0,0 +1,103 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import UIKit
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
// MARK: - Habitat
enum Habitat: String, CaseIterable, ImportableAttributeType, FieldStorableType {
case water = "water"
case land = "land"
case amphibian = "amphibian"
}
// MARK: CustomStringConvertible
var description: String {
return """
dnaCode: \(self.dnaCode)
numberOfLimbs: \(self.numberOfLimbs)
hasVertebrae: \(self.hasVertebrae)
hasHead: \(self.hasHead)
hasTail: \(self.hasTail)
habitat: \(self.habitat)
hasWings: \(self.hasWings)
"""
}
// MARK: Advanced.EvolutionDemo.CreatureType
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
return .init(
listPublisher: dataStack.publishList(
From<Advanced.EvolutionDemo.V3.Creature>()
.orderBy(.descending(\.$dnaCode))
),
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>()
)
}
func mutate(in transaction: BaseDataTransaction) {
self.numberOfLimbs = .random(in: 1...4) * 2
self.hasVertebrae = .random()
self.hasHead = true
self.hasTail = .random()
self.habitat = Habitat.allCases.randomElement()!
self.hasWings = .random()
}
}
}

View File

@@ -0,0 +1,43 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Advanced.EvolutionDemo.V3
extension Advanced.EvolutionDemo.V3 {
// MARK: - Advanced.EvolutionDemo.V3.FromV2
enum FromV2 {
// MARK: Internal
static var mapping: CustomSchemaMappingProvider {
return CustomSchemaMappingProvider(
from: Advanced.EvolutionDemo.V2.name,
to: Advanced.EvolutionDemo.V3.name,
entityMappings: [
.transformEntity(
sourceEntity: "Creature",
destinationEntity: "Creature",
transformer: { (source, createDestination) in
let destination = createDestination()
destination["dnaCode"] = source["dnaCode"]
destination["numberOfLimbs"] = source["numberOfFlippers"]
destination["hasVertebrae"] = source["hasVertebrae"]
destination["hasHead"] = source["hasHead"]
destination["hasTail"] = source["hasTail"]
destination["hasWings"] = Bool.random()
destination["habitat"] = Advanced.EvolutionDemo.V3.Creature.Habitat.allCases.randomElement()!.rawValue
}
)
]
)
}
}
}

View File

@@ -0,0 +1,33 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Advanced.EvolutionDemo.V3
extension Advanced.EvolutionDemo.V3 {
// MARK: - Advanced.EvolutionDemo.V3.FromV4
enum FromV4 {
// MARK: Internal
static var mapping: CustomSchemaMappingProvider {
return CustomSchemaMappingProvider(
from: Advanced.EvolutionDemo.V4.name,
to: Advanced.EvolutionDemo.V3.name,
entityMappings: [
.transformEntity(
sourceEntity: "Creature",
destinationEntity: "Creature",
transformer: CustomSchemaMappingProvider.CustomMapping.inferredTransformation(_:_:)
)
]
)
}
}
}

View File

@@ -0,0 +1,23 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.V3
/**
Namespace for V3 models (`Advanced.EvolutionDemo.GeologicalPeriod.ageOfReptiles`)
*/
enum V3 {
// MARK: Internal
static let name: ModelVersion = "Advanced.EvolutionDemo.V3"
}
}

View File

@@ -0,0 +1,100 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import UIKit
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
// MARK: CustomStringConvertible
var description: String {
return """
dnaCode: \(self.dnaCode)
numberOfLimbs: \(self.numberOfLimbs)
hasVertebrae: \(self.hasVertebrae)
hasHead: \(self.hasHead)
hasTail: \(self.hasTail)
habitat: \(self.habitat)
hasWings: \(self.hasWings)
"""
}
// MARK: Advanced.EvolutionDemo.CreatureType
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
return .init(
listPublisher: dataStack.publishList(
From<Advanced.EvolutionDemo.V4.Creature>()
.orderBy(.descending(\.$dnaCode))
),
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>()
)
}
func mutate(in transaction: BaseDataTransaction) {
self.numberOfLimbs = .random(in: 1...4) * 2
self.hasVertebrae = .random()
self.hasHead = true
self.hasTail = .random()
self.habitat = Habitat.allCases.randomElement()!
self.hasWings = .random()
self.isWarmBlooded = .random()
}
}
}

View File

@@ -0,0 +1,44 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Advanced.EvolutionDemo.V4
extension Advanced.EvolutionDemo.V4 {
// MARK: - Advanced.EvolutionDemo.V4.FromV3
enum FromV3 {
// MARK: Internal
static var mapping: CustomSchemaMappingProvider {
return CustomSchemaMappingProvider(
from: Advanced.EvolutionDemo.V3.name,
to: Advanced.EvolutionDemo.V4.name,
entityMappings: [
.transformEntity(
sourceEntity: "Creature",
destinationEntity: "Creature",
transformer: { (source, createDestination) in
let destination = createDestination()
destination.enumerateAttributes { (destinationAttribute, sourceAttribute) in
if let sourceAttribute = sourceAttribute {
destination[destinationAttribute] = source[sourceAttribute]
}
}
destination["isWarmBlooded"] = Bool.random()
}
)
]
)
}
}
}

View File

@@ -0,0 +1,23 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.V4
/**
Namespace for V3 models (`Advanced.EvolutionDemo.GeologicalPeriod.ageOfMammals`)
*/
enum V4 {
// MARK: Internal
static let name: ModelVersion = "Advanced.EvolutionDemo.V4"
}
}

View File

@@ -0,0 +1,24 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
// MARK: - Advanced
extension Advanced {
// MARK: - Advanced.EvolutionDemo
/**
Sample execution of progressive migrations. This example demonstrates the following concepts:
- How to inspect the current model version of the store (if it exists)
- How to do two-way migration chains (upgrades + downgrades)
- How to support multiple versions of the model on the same app
- How to migrate between `NSManagedObject` schema (`xcdatamodel` files) and `CoreStoreObject` schema.
- How to use `XcodeSchemaMappingProvider`s for `NSManagedObject` stores, and `CustomSchemaMappingProvider`s for `CoreStoreObject` stores
- How to manage migration models using namespacing technique
Note that ideally, your app should be supporting just the latest version of the model, and provide one-way progressive migrations from all the earlier versions.
*/
enum EvolutionDemo {}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19F101" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Creature" representedClassName="Advanced_EvolutionDemo_V1_Creature" syncable="YES" codeGenerationType="class">
<attribute name="dnaCode" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numberOfFlagella" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
<elements>
<element name="Creature" positionX="-36" positionY="9" width="128" height="73"/>
</elements>
</model>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19F101" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Creature" representedClassName="Advanced_EvolutionDemo_V1_Creature" syncable="YES" codeGenerationType="class">
<attribute name="dnaCode" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numberOfFlagella" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
<elements>
<element name="Creature" positionX="-36" positionY="9" width="128" height="73"/>
</elements>
</model>

View File

@@ -0,0 +1,10 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
// MARK: - Classic
/**
Sample usages for `NSManagedObject` subclasses
*/
enum Classic {}

View File

@@ -0,0 +1,79 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Combine
import CoreStore
import SwiftUI
// MARK: - Classic.ColorsDemo
extension Classic.ColorsDemo {
// MARK: - Classic.ColorsDemo.DetailView
struct DetailView: UIViewControllerRepresentable {
// MARK: Internal
init(_ palette: ObjectMonitor<Classic.ColorsDemo.Palette>) {
self.palette = palette
}
// 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> {
return self.palette
}
// MARK: Private
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

View File

@@ -0,0 +1,291 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Classic.ColorsDemo
extension Classic.ColorsDemo {
// MARK: - Classic.ColorsDemo.DetailViewController
final class DetailViewController: UIViewController, ObjectObserver {
/**
Sample 1: We can normally use `ObjectPublisher` directly, which is simpler. But for this demo, we will be using `ObjectMonitor` instead because we need to keep track of which properties change to prevent our `UISlider` from stuttering. Refer to the `objectMonitor(_:didUpdateObject:changedPersistentKeys:)` implementation below.
*/
var palette: ObjectMonitor<Classic.ColorsDemo.Palette> {
didSet {
oldValue.removeObserver(self)
self.startMonitoringObject()
}
}
init(_ palette: ObjectMonitor<Classic.ColorsDemo.Palette>) {
self.palette = palette
super.init(nibName: nil, bundle: nil)
}
/**
Sample 2: Once the views are created, we can start receiving `ObjectMonitor` updates in our `ObjectObserver` conformance methods. We typically call this at the end of `viewDidLoad`. Note that after the `addObserver` call, only succeeding updates will trigger our `ObjectObserver` methods, so to immediately display the current values, we need to initialize our views once (in this case, using `reloadPaletteInfo(_:changedKeys:)`.
*/
private func startMonitoringObject() {
self.palette.addObserver(self)
if let palette = self.palette.object {
self.reloadPaletteInfo(palette, changedKeys: nil)
}
}
/**
Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ObjectMonitor`s safely remove deallocated observers automatically.
*/
deinit {
self.palette.removeObserver(self)
}
/**
Sample 4: Our `objectMonitor(_:didUpdateObject:changedPersistentKeys:)` implementation passes a `Set<KeyPathString>` to our reload method. We can then inspect which values were triggered by each `UISlider`, so we can avoid double-updates that can lag the `UISlider` dragging.
*/
func reloadPaletteInfo(
_ palette: Classic.ColorsDemo.Palette,
changedKeys: Set<KeyPathString>?
) {
self.view.backgroundColor = palette.color
self.hueLabel.text = "H: \(Int(palette.hue * 359))°"
self.saturationLabel.text = "S: \(Int(palette.saturation * 100))%"
self.brightnessLabel.text = "B: \(Int(palette.brightness * 100))%"
if changedKeys == nil
|| changedKeys?.contains(String(keyPath: \Classic.ColorsDemo.Palette.hue)) == true {
self.hueSlider.value = Float(palette.hue)
}
if changedKeys == nil
|| changedKeys?.contains(String(keyPath: \Classic.ColorsDemo.Palette.saturation)) == true {
self.saturationSlider.value = palette.saturation
}
if changedKeys == nil
|| changedKeys?.contains(String(keyPath: \Classic.ColorsDemo.Palette.brightness)) == true {
self.brightnessSlider.value = palette.brightness
}
}
// MARK: ObjectObserver
func objectMonitor(
_ monitor: ObjectMonitor<Classic.ColorsDemo.Palette>,
didUpdateObject object: Classic.ColorsDemo.Palette,
changedPersistentKeys: Set<KeyPathString>
) {
self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys)
}
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
let view = self.view!
let containerView = UIView()
do {
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = UIColor.white
containerView.layer.cornerRadius = 10
containerView.layer.masksToBounds = true
containerView.layer.shadowColor = UIColor(white: 0.5, alpha: 0.3).cgColor
containerView.layer.shadowOffset = .init(width: 1, height: 1)
containerView.layer.shadowRadius = 2
view.addSubview(containerView)
}
let vStackView = UIStackView()
do {
vStackView.translatesAutoresizingMaskIntoConstraints = false
vStackView.axis = .vertical
vStackView.spacing = 10
vStackView.distribution = .fill
vStackView.alignment = .fill
containerView.addSubview(vStackView)
}
let palette = self.palette.object
let rows: [(label: UILabel, slider: UISlider, initialValue: Float, sliderValueChangedSelector: Selector)] = [
(
self.hueLabel,
self.hueSlider,
palette?.hue ?? 0,
#selector(self.hueSliderValueDidChange(_:))
),
(
self.saturationLabel,
self.saturationSlider,
palette?.saturation ?? 0,
#selector(self.saturationSliderValueDidChange(_:))
),
(
self.brightnessLabel,
self.brightnessSlider,
palette?.brightness ?? 0,
#selector(self.brightnessSliderValueDidChange(_:))
)
]
for (label, slider, initialValue, sliderValueChangedSelector) in rows {
let hStackView = UIStackView()
do {
hStackView.translatesAutoresizingMaskIntoConstraints = false
hStackView.axis = .horizontal
hStackView.spacing = 5
hStackView.distribution = .fill
hStackView.alignment = .center
vStackView.addArrangedSubview(hStackView)
}
do {
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = UIColor(white: 0, alpha: 0.8)
label.textAlignment = .center
hStackView.addArrangedSubview(label)
}
do {
slider.translatesAutoresizingMaskIntoConstraints = false
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = initialValue
slider.addTarget(
self,
action: sliderValueChangedSelector,
for: .valueChanged
)
hStackView.addArrangedSubview(slider)
}
}
layout: do {
NSLayoutConstraint.activate(
[
containerView.leadingAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.leadingAnchor,
constant: 10
),
containerView.bottomAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: -10
),
containerView.trailingAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.trailingAnchor,
constant: -10
),
vStackView.topAnchor.constraint(
equalTo: containerView.topAnchor,
constant: 15
),
vStackView.leadingAnchor.constraint(
equalTo: containerView.leadingAnchor,
constant: 15
),
vStackView.bottomAnchor.constraint(
equalTo: containerView.bottomAnchor,
constant: -15
),
vStackView.trailingAnchor.constraint(
equalTo: containerView.trailingAnchor,
constant: -15
)
]
)
NSLayoutConstraint.activate(
rows.map { label, _, _, _ in
label.widthAnchor.constraint(equalToConstant: 80)
}
)
}
self.startMonitoringObject()
}
// MARK: Private
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError()
}
private let hueLabel: UILabel = .init()
private let saturationLabel: UILabel = .init()
private let brightnessLabel: UILabel = .init()
private let hueSlider: UISlider = .init()
private let saturationSlider: UISlider = .init()
private let brightnessSlider: UISlider = .init()
@objc
private dynamic func hueSliderValueDidChange(_ sender: UISlider) {
let value = sender.value
Classic.ColorsDemo.dataStack.perform(
asynchronous: { [weak self] (transaction) in
let palette = transaction.edit(self?.palette.object)
palette?.hue = value
},
completion: { _ in }
)
}
@objc
private dynamic func saturationSliderValueDidChange(_ sender: UISlider) {
let value = sender.value
Classic.ColorsDemo.dataStack.perform(
asynchronous: { [weak self] (transaction) in
let palette = transaction.edit(self?.palette.object)
palette?.saturation = value
},
completion: { _ in }
)
}
@objc
private dynamic func brightnessSliderValueDidChange(_ sender: UISlider) {
let value = sender.value
Classic.ColorsDemo.dataStack.perform(
asynchronous: { [weak self] (transaction) in
let palette = transaction.edit(self?.palette.object)
palette?.brightness = value
},
completion: { _ in }
)
}
}
}

View File

@@ -0,0 +1,36 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Classic.ColorsDemo
extension Classic.ColorsDemo {
// MARK: - Classic.ColorsDemo.Filter
enum Filter: String, CaseIterable {
case all = "All Colors"
case light = "Light Colors"
case dark = "Dark Colors"
func next() -> Filter {
let allCases = Self.allCases
return allCases[(allCases.firstIndex(of: self)! + 1) % allCases.count]
}
func whereClause() -> Where<Classic.ColorsDemo.Palette> {
switch self {
case .all: return .init()
case .light: return (\.brightness >= 0.6)
case .dark: return (\.brightness <= 0.4)
}
}
}
}

View File

@@ -0,0 +1,28 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Classic.ColorsDemo
extension Classic.ColorsDemo {
// MARK: - Classic.ColorsDemo.ItemCell
final class ItemCell: UITableViewCell {
// MARK: Internal
static let reuseIdentifier: String = NSStringFromClass(Classic.ColorsDemo.ItemCell.self)
func setPalette(_ palette: Classic.ColorsDemo.Palette) {
self.contentView.backgroundColor = palette.color
self.textLabel?.text = palette.colorText
self.textLabel?.textColor = palette.brightness > 0.6 ? .black : .white
}
}
}

View File

@@ -0,0 +1,89 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import SwiftUI
// MARK: - Classic.ColorsDemo
extension Classic.ColorsDemo {
// MARK: - Classic.ColorsDemo.ListView
struct ListView: UIViewControllerRepresentable {
// MARK: Internal
init(
listMonitor: ListMonitor<Classic.ColorsDemo.Palette>,
onPaletteTapped: @escaping (Classic.ColorsDemo.Palette) -> Void
) {
self.listMonitor = listMonitor
self.onPaletteTapped = onPaletteTapped
}
// MARK: UIViewControllerRepresentable
typealias UIViewControllerType = Classic.ColorsDemo.ListViewController
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
return UIViewControllerType(
listMonitor: self.listMonitor,
onPaletteTapped: self.onPaletteTapped
)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
uiViewController.setEditing(
context.environment.editMode?.wrappedValue.isEditing == true,
animated: true
)
}
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
// MARK: Private
private let listMonitor: ListMonitor<Classic.ColorsDemo.Palette>
private let onPaletteTapped: (Classic.ColorsDemo.Palette) -> Void
}
}
#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
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.ListView(
listMonitor: Classic.ColorsDemo.palettesMonitor,
onPaletteTapped: { _ in }
)
}
}
#endif

View File

@@ -0,0 +1,199 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Classic.ColorsDemo
extension Classic.ColorsDemo {
// MARK: - Classic.ColorsDemo.ListViewController
final class ListViewController: UITableViewController, ListSectionObserver {
/**
Sample 1: Once the views are created, we can start observing `ListMonitor` updates. We typically call this at the end of `viewDidLoad`. Note that the `addObserver`'s closure argument will only be called on the succeeding updates, so to immediately display the current values, we need to call `tableView.reloadData()` once.
*/
private func startObservingList() {
self.listMonitor.addObserver(self)
self.tableView.reloadData()
}
/**
Sample 2: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ListMonitor`s safely remove deallocated observers automatically.
*/
deinit {
self.listMonitor.removeObserver(self)
}
/**
Sample 3: `ListSectionObserver` (and inherently, `ListObjectObserver` and `ListObserver`) conformance
*/
// MARK: ListObserver
typealias ListEntityType = Classic.ColorsDemo.Palette
func listMonitorWillChange(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>) {
self.tableView.beginUpdates()
}
func listMonitorDidChange(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>) {
self.tableView.endUpdates()
}
func listMonitorDidRefetch(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>) {
self.tableView.reloadData()
}
// MARK: ListObjectObserver
func listMonitor(_ monitor: ListMonitor<ListEntityType>, didInsertObject object: ListEntityType, toIndexPath indexPath: IndexPath) {
self.tableView.insertRows(at: [indexPath], with: .automatic)
}
func listMonitor(_ monitor: ListMonitor<ListEntityType>, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath) {
self.tableView.deleteRows(at: [indexPath], with: .automatic)
}
func listMonitor(_ monitor: ListMonitor<ListEntityType>, didUpdateObject object: ListEntityType, atIndexPath indexPath: IndexPath) {
if case let cell as Classic.ColorsDemo.ItemCell = self.tableView.cellForRow(at: indexPath) {
cell.setPalette(object)
}
}
func listMonitor(_ monitor: ListMonitor<ListEntityType>, didMoveObject object: ListEntityType, fromIndexPath: IndexPath, toIndexPath: IndexPath) {
self.tableView.deleteRows(at: [fromIndexPath], with: .automatic)
self.tableView.insertRows(at: [toIndexPath], with: .automatic)
}
// MARK: ListSectionObserver
func listMonitor(_ monitor: ListMonitor<ListEntityType>, didInsertSection sectionInfo: NSFetchedResultsSectionInfo, toSectionIndex sectionIndex: Int) {
self.tableView.insertSections(IndexSet(integer: sectionIndex), with: .automatic)
}
func listMonitor(_ monitor: ListMonitor<ListEntityType>, didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, fromSectionIndex sectionIndex: Int) {
self.tableView.deleteSections(IndexSet(integer: sectionIndex), with: .automatic)
}
// MARK: UITableViewDataSource
override func numberOfSections(in tableView: UITableView) -> Int {
return self.listMonitor.numberOfSections()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.listMonitor.numberOfObjects(in: section)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: Classic.ColorsDemo.ItemCell.reuseIdentifier,
for: indexPath
) as! Classic.ColorsDemo.ItemCell
cell.setPalette(self.listMonitor[indexPath])
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.listMonitor.sectionInfo(at: section).name
}
// MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
switch editingStyle {
case .delete:
let object = self.listMonitor[indexPath]
Classic.ColorsDemo.dataStack.perform(
asynchronous: { (transaction) in
transaction.delete(object)
},
completion: { _ in }
)
default:
break
}
}
// MARK: Internal
init(
listMonitor: ListMonitor<Classic.ColorsDemo.Palette>,
onPaletteTapped: @escaping (Classic.ColorsDemo.Palette) -> Void
) {
self.listMonitor = listMonitor
self.onPaletteTapped = onPaletteTapped
super.init(style: .plain)
}
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(
Classic.ColorsDemo.ItemCell.self,
forCellReuseIdentifier: Classic.ColorsDemo.ItemCell.reuseIdentifier
)
self.startObservingList()
}
// MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.onPaletteTapped(
self.listMonitor[indexPath]
)
}
// MARK: Private
private let listMonitor: ListMonitor<Classic.ColorsDemo.Palette>
private let onPaletteTapped: (Classic.ColorsDemo.Palette) -> Void
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError()
}
}
}

View File

@@ -0,0 +1,247 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import SwiftUI
// MARK: - Classic.ColorsDemo
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._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)
)
}
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") }
)
}
)
}
// MARK: Private
private let listMonitor: ListMonitor<Classic.ColorsDemo.Palette>
@ObservedObject
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
try transaction.deleteAll(From<Classic.ColorsDemo.Palette>())
},
completion: { _ in }
)
}
private func addColor() {
Classic.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
_ = transaction.create(Into<Classic.ColorsDemo.Palette>())
},
completion: { _ in }
)
}
private func shuffleColors() {
Classic.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
for palette in try transaction.fetchAll(From<Classic.ColorsDemo.Palette>()) {
palette.setRandomHue()
}
},
completion: { _ in }
)
}
// MARK: - Classic.ColorsDemo.MainView.ListHelper
fileprivate final class ListHelper: ObservableObject, 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 {
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 {
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: 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

View File

@@ -0,0 +1,101 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreData
import UIKit
// MARK: - Classic.ColorsDemo.Palette
@objc(Classic_ColorsDemo_Palette)
final class Classic_ColorsDemo_Palette: NSManagedObject {
// MARK: Internal
@NSManaged
dynamic var hue: Float
@NSManaged
dynamic var saturation: Float
@NSManaged
dynamic var brightness: Float
@objc
dynamic var colorGroup: String! {
let key = #keyPath(colorGroup)
if case let value as String = self.getValue(forKvcKey: key) {
return value
}
let newValue: String
switch self.hue * 359 {
case 0 ..< 20: newValue = "Lower Reds"
case 20 ..< 57: newValue = "Oranges and Browns"
case 57 ..< 90: newValue = "Yellow-Greens"
case 90 ..< 159: newValue = "Greens"
case 159 ..< 197: newValue = "Blue-Greens"
case 197 ..< 241: newValue = "Blues"
case 241 ..< 297: newValue = "Violets"
case 297 ..< 331: newValue = "Magentas"
default: newValue = "Upper Reds"
}
self.setPrimitiveValue(newValue, forKey: key)
return newValue
}
var color: UIColor {
let newValue = UIColor(
hue: CGFloat(self.hue),
saturation: CGFloat(self.saturation),
brightness: CGFloat(self.brightness),
alpha: 1.0
)
return newValue
}
var colorText: String {
let newValue: String = "H: \(self.hue * 359)˚, S: \(round(self.saturation * 100.0))%, B: \(round(self.brightness * 100.0))%"
return newValue
}
func setRandomHue() {
self.hue = Self.randomHue()
}
// MARK: NSManagedObject
public override func awakeFromInsert() {
super.awakeFromInsert()
self.hue = Self.randomHue()
self.saturation = Self.randomSaturation()
self.brightness = Self.randomBrightness()
}
// MARK: Private
private static func randomHue() -> Float {
return Float.random(in: 0.0 ... 1.0)
}
private static func randomSaturation() -> Float {
return Float.random(in: 0.4 ... 1.0)
}
private static func randomBrightness() -> Float {
return Float.random(in: 0.1 ... 0.9)
}
}

View File

@@ -0,0 +1,59 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Classic
extension Classic {
// MARK: - Classic.ColorsDemo
/**
Sample usages for observing lists or single instances of `NSManagedObject`s
*/
enum ColorsDemo {
// MARK: Internal
typealias Palette = Classic_ColorsDemo_Palette
static let dataStack: DataStack = {
let dataStack = DataStack(
xcodeModelName: "Classic.ColorsDemo",
bundle: Bundle(for: Palette.self)
)
/**
- Important: `addStorageAndWait(_:)` was used here to simplify initializing the demo, but in practice the asynchronous function variants are recommended.
*/
try! dataStack.addStorageAndWait(
SQLiteStore(
fileName: "Classic.ColorsDemo.sqlite",
localStorageOptions: .recreateStoreOnModelMismatch
)
)
return dataStack
}()
static let palettesMonitor: ListMonitor<Classic.ColorsDemo.Palette> = Classic.ColorsDemo.dataStack.monitorSectionedList(
From<Classic.ColorsDemo.Palette>()
.sectionBy(\.colorGroup)
.where(Classic.ColorsDemo.filter.whereClause())
.orderBy(.ascending(\.hue))
)
static var filter: Classic.ColorsDemo.Filter = .all {
didSet {
Classic.ColorsDemo.palettesMonitor.refetch(
self.filter.whereClause(),
OrderBy<Classic.ColorsDemo.Palette>(.ascending(\.hue))
)
}
}
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20C5048l" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Palette" representedClassName="Classic_ColorsDemo_Palette" syncable="YES">
<attribute name="brightness" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="colorGroup" optional="YES" transient="YES" attributeType="String"/>
<attribute name="hue" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="saturation" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<fetchedProperty name="testFetchProperty" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="Palette"/>
</fetchedProperty>
</entity>
<elements>
<element name="Palette" positionX="-63" positionY="-18" width="128" height="110"/>
</elements>
</model>

View File

@@ -0,0 +1,36 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Modern.ColorsDemo
extension Modern.ColorsDemo {
// MARK: - Modern.ColorsDemo.Filter
enum Filter: String, CaseIterable {
case all = "All Colors"
case light = "Light Colors"
case dark = "Dark Colors"
func next() -> Filter {
let allCases = Self.allCases
return allCases[(allCases.firstIndex(of: self)! + 1) % allCases.count]
}
func whereClause() -> Where<Modern.ColorsDemo.Palette> {
switch self {
case .all: return .init()
case .light: return (\.$brightness >= 0.6)
case .dark: return (\.$brightness <= 0.4)
}
}
}
}

View File

@@ -0,0 +1,177 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import SwiftUI
// MARK: - Modern.ColorsDemo
extension Modern.ColorsDemo {
// MARK: - Modern.ColorsDemo.MainView
struct MainView<ListView: View, DetailView: View>: View {
// MARK: Internal
init(
listView: @escaping (
_ listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
_ onPaletteTapped: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
) -> ListView,
detailView: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> DetailView) {
self.listView = listView
self.detailView = detailView
}
// 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 {
self.detailView($0)
.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") }
)
}
)
}
// MARK: Private
@ListState(Modern.ColorsDemo.palettesPublisher)
private var palettes: ListSnapshot
private let listView: (
_ listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
_ onPaletteTapped: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
) -> ListView
private let detailView: (
_ objectPublisher: ObjectPublisher<Modern.ColorsDemo.Palette>
) -> DetailView
@State
private var selectedPalette: ObjectPublisher<Modern.ColorsDemo.Palette>?
@State
private var filter: Modern.ColorsDemo.Filter = Modern.ColorsDemo.filter
private func changeFilter() {
Modern.ColorsDemo.filter = Modern.ColorsDemo.filter.next()
self.filter = Modern.ColorsDemo.filter
}
private func clearColors() {
Modern.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
try transaction.deleteAll(From<Modern.ColorsDemo.Palette>())
},
sourceIdentifier: TransactionSource.clear,
completion: { _ in }
)
}
private func addColor() {
Modern.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
_ = transaction.create(Into<Modern.ColorsDemo.Palette>())
},
sourceIdentifier: TransactionSource.add,
completion: { _ in }
)
}
private func shuffleColors() {
Modern.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
for palette in try transaction.fetchAll(From<Modern.ColorsDemo.Palette>()) {
palette.setRandomHue()
}
},
sourceIdentifier: TransactionSource.shuffle,
completion: { _ in }
)
}
}
}
#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
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()
}
}
)
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

View File

@@ -0,0 +1,143 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import UIKit
import CoreStore
// MARK: - Modern.ColorsDemo
extension Modern.ColorsDemo {
// MARK: - Modern.ColorsDemo.Palette
final class Palette: CoreStoreObject {
// MARK: Internal
@Field.Stored(
"hue",
customSetter: { object, field, value in
Palette.resetVirtualProperties(object)
field.primitiveValue = value
},
dynamicInitialValue: { Palette.randomHue() }
)
var hue: Float
@Field.Stored(
"saturation",
customSetter: { object, field, value in
Palette.resetVirtualProperties(object)
field.primitiveValue = value
},
dynamicInitialValue: { Palette.randomSaturation() }
)
var saturation: Float
@Field.Stored(
"brightness",
customSetter: { object, field, value in
Palette.resetVirtualProperties(object)
field.primitiveValue = value
},
dynamicInitialValue: { Palette.randomBrightness() }
)
var brightness: Float
@Field.Virtual(
"colorGroup",
customGetter: { object, field in
if let colorGroup = field.primitiveValue {
return colorGroup
}
let colorGroup: String
switch object.$hue.value * 359 {
case 0 ..< 20: colorGroup = "Lower Reds"
case 20 ..< 57: colorGroup = "Oranges and Browns"
case 57 ..< 90: colorGroup = "Yellow-Greens"
case 90 ..< 159: colorGroup = "Greens"
case 159 ..< 197: colorGroup = "Blue-Greens"
case 197 ..< 241: colorGroup = "Blues"
case 241 ..< 297: colorGroup = "Violets"
case 297 ..< 331: colorGroup = "Magentas"
default: colorGroup = "Upper Reds"
}
field.primitiveValue = colorGroup
return colorGroup
}
)
var colorGroup: String
@Field.Virtual(
"color",
customGetter: { object, field in
if let color = field.primitiveValue {
return color
}
let color = UIColor(
hue: CGFloat(object.$hue.value),
saturation: CGFloat(object.$saturation.value),
brightness: CGFloat(object.$brightness.value),
alpha: 1.0
)
field.primitiveValue = color
return color
}
)
var color: UIColor
@Field.Virtual(
"colorText",
customGetter: { object, field in
if let colorText = field.primitiveValue {
return colorText
}
let colorText = "H: \(object.$hue.value * 359)˚, S: \(round(object.$saturation.value * 100.0))%, B: \(round(object.$brightness.value * 100.0))%"
field.primitiveValue = colorText
return colorText
}
)
var colorText: String
func setRandomHue() {
self.hue = Self.randomHue()
}
// MARK: Private
private static func resetVirtualProperties(_ object: ObjectProxy<Modern.ColorsDemo.Palette>) {
object.$colorGroup.primitiveValue = nil
object.$color.primitiveValue = nil
object.$colorText.primitiveValue = nil
}
private static func randomHue() -> Float {
return Float.random(in: 0.0 ... 1.0)
}
private static func randomSaturation() -> Float {
return Float.random(in: 0.4 ... 1.0)
}
private static func randomBrightness() -> Float {
return Float.random(in: 0.1 ... 0.9)
}
}
}

View File

@@ -0,0 +1,160 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Combine
import CoreStore
import SwiftUI
// MARK: - Modern.ColorsDemo.SwiftUI
extension Modern.ColorsDemo.SwiftUI {
// MARK: - Modern.ColorsDemo.SwiftUI.DetailView
struct DetailView: View {
/**
Sample 1: Using a `ObjectState` to observe object changes. Note that the `ObjectSnapshot` is always `Optional`
*/
@ObjectState
private var palette: ObjectSnapshot<Modern.ColorsDemo.Palette>?
/**
Sample 2: Setting properties that can be binded to controls (`Slider` in this case) by creating custom `@Binding` instances that updates the store when the values change.
*/
@Binding
private var hue: Float
@Binding
private var saturation: Float
@Binding
private var brightness: Float
init(_ palette: ObjectPublisher<Modern.ColorsDemo.Palette>) {
self._palette = .init(palette)
self._hue = Binding(
get: { palette.hue ?? 0 },
set: { percentage in
Modern.ColorsDemo.dataStack.perform(
asynchronous: { (transaction) in
let palette = palette.asEditable(in: transaction)
palette?.hue = percentage
},
completion: { _ in }
)
}
)
self._saturation = Binding(
get: { palette.saturation ?? 0 },
set: { percentage in
Modern.ColorsDemo.dataStack.perform(
asynchronous: { (transaction) in
let palette = palette.asEditable(in: transaction)
palette?.saturation = percentage
},
completion: { _ in }
)
}
)
self._brightness = Binding(
get: { palette.brightness ?? 0 },
set: { percentage in
Modern.ColorsDemo.dataStack.perform(
asynchronous: { (transaction) in
let palette = palette.asEditable(in: transaction)
palette?.brightness = percentage
},
completion: { _ in }
)
}
)
}
// MARK: View
var body: some View {
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)
Slider(
value: self.$hue,
in: 0 ... 1,
step: 1 / 359
)
}
HStack {
Text("S: \(Int(palette.$saturation * 100))%")
.frame(width: 80)
Slider(
value: self.$saturation,
in: 0 ... 1,
step: 1 / 100
)
}
HStack {
Text("B: \(Int(palette.$brightness * 100))%")
.frame(width: 80)
Slider(
value: self.$brightness,
in: 0 ... 1,
step: 1 / 100
)
}
}
.foregroundColor(Color(.sRGB, white: 0, opacity: 0.8))
.padding()
}
.fixedSize(horizontal: false, vertical: true)
.padding()
}
}
}
}
}
#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

View File

@@ -0,0 +1,74 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import SwiftUI
// MARK: - Modern.ColorsDemo.SwiftUI
extension Modern.ColorsDemo.SwiftUI {
// MARK: - Modern.ColorsDemo.SwiftUI.ItemView
struct ItemView: View {
/**
Sample 1: Using a `ObjectState` to observe object changes. Note that the `ObjectSnapshot` is always `Optional`
*/
@ObjectState
private var palette: ObjectSnapshot<Modern.ColorsDemo.Palette>?
/**
Sample 2: Initializing a `ObjectState` from an existing `ObjectPublisher`
*/
internal init(_ palette: ObjectPublisher<Modern.ColorsDemo.Palette>) {
self._palette = .init(palette)
}
/**
Sample 3: Readding values directly from the `ObjectSnapshot`
*/
var body: some View {
if let palette = self.palette {
Color(palette.$color).overlay(
Text(palette.$colorText)
.foregroundColor(palette.$brightness > 0.6 ? .black : .white)
.padding(),
alignment: .leading
)
.animation(.default)
}
}
}
}
#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

View File

@@ -0,0 +1,124 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import SwiftUI
// MARK: - Modern.ColorsDemo.SwiftUI
extension Modern.ColorsDemo.SwiftUI {
// MARK: - Modern.ColorsDemo.SwiftUI.ListView
struct ListView: View {
/**
Sample 1: Using a `ListState` to observe list changes
*/
@ListState
private var palettes: ListSnapshot<Modern.ColorsDemo.Palette>
/**
Sample 2: Initializing a `ListState` from an existing `ListPublisher`
*/
init(
listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
onPaletteTapped: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
) {
self._palettes = .init(listPublisher)
self.onPaletteTapped = onPaletteTapped
}
/**
Sample 3: Assigning sections and items of the `ListSnapshot` to corresponding `View`s by using the correct `ForEach` overloads.
*/
var body: some View {
List {
ForEach(sectionIn: self.palettes) { section in
Section(header: Text(section.sectionID)) {
ForEach(objectIn: section) { palette in
Button(
action: {
self.onPaletteTapped(palette)
},
label: {
Modern.ColorsDemo.SwiftUI.ItemView(palette)
}
)
.listRowInsets(.init())
}
.onDelete { itemIndices in
self.deleteColors(at: itemIndices, in: section.sectionID)
}
}
}
}
.animation(.default)
.listStyle(PlainListStyle())
.edgesIgnoringSafeArea([])
}
// MARK: Private
private let onPaletteTapped: (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
private func deleteColors(at indices: IndexSet, in sectionID: String) {
let objectIDsToDelete = self.palettes.itemIDs(
inSectionWithID: sectionID,
atIndices: indices
)
Modern.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
transaction.delete(objectIDs: objectIDsToDelete)
},
completion: { _ in }
)
}
}
}
#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
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()
}
}
)
return Modern.ColorsDemo.SwiftUI.ListView(
listPublisher: Modern.ColorsDemo.palettesPublisher,
onPaletteTapped: { _ in }
)
}
}
#endif

View File

@@ -0,0 +1,13 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
// MARK: - Modern.ColorsDemo
extension Modern.ColorsDemo {
// MARK: - SwiftUI
enum SwiftUI {}
}

View File

@@ -0,0 +1,74 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Combine
import CoreStore
import SwiftUI
// MARK: - Modern.ColorsDemo.UIKit
extension Modern.ColorsDemo.UIKit {
// MARK: - Modern.ColorsDemo.UIKit.DetailView
struct DetailView: UIViewControllerRepresentable {
// MARK: Internal
init(_ palette: ObjectPublisher<Modern.ColorsDemo.Palette>) {
self.palette = palette
}
// 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) {}
// MARK: Private
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

View File

@@ -0,0 +1,292 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Modern.ColorsDemo.UIKit
extension Modern.ColorsDemo.UIKit {
// MARK: - Modern.ColorsDemo.UIKit.DetailViewController
final class DetailViewController: UIViewController, ObjectObserver {
/**
Sample 1: We can normally use `ObjectPublisher` directly, which is simpler. But for this demo, we will be using `ObjectMonitor` instead because we need to keep track of which properties change to prevent our `UISlider` from stuttering. Refer to the `objectMonitor(_:didUpdateObject:changedPersistentKeys:)` implementation below.
*/
init(_ palette: ObjectPublisher<Modern.ColorsDemo.Palette>) {
self.palette = Modern.ColorsDemo.dataStack.monitorObject(
palette.object!
)
super.init(nibName: nil, bundle: nil)
}
/**
Sample 2: Once the views are created, we can start receiving `ObjectMonitor` updates in our `ObjectObserver` conformance methods. We typically call this at the end of `viewDidLoad`. Note that after the `addObserver` call, only succeeding updates will trigger our `ObjectObserver` methods, so to immediately display the current values, we need to initialize our views once (in this case, using `reloadPaletteInfo(_:changedKeys:)`.
*/
private func startMonitoringObject() {
self.palette.addObserver(self)
if let palette = self.palette.object {
self.reloadPaletteInfo(palette, changedKeys: nil)
}
}
/**
Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ObjectMonitor`s safely remove deallocated observers automatically.
*/
var palette: ObjectMonitor<Modern.ColorsDemo.Palette> {
didSet {
oldValue.removeObserver(self)
self.startMonitoringObject()
}
}
deinit {
self.palette.removeObserver(self)
}
/**
Sample 4: Our `objectMonitor(_:didUpdateObject:changedPersistentKeys:)` implementation passes a `Set<KeyPathString>` to our reload method. We can then inspect which values were triggered by each `UISlider`, so we can avoid double-updates that can lag the `UISlider` dragging.
*/
func reloadPaletteInfo(
_ palette: Modern.ColorsDemo.Palette,
changedKeys: Set<KeyPathString>?
) {
self.view.backgroundColor = palette.color
self.hueLabel.text = "H: \(Int(palette.hue * 359))°"
self.saturationLabel.text = "S: \(Int(palette.saturation * 100))%"
self.brightnessLabel.text = "B: \(Int(palette.brightness * 100))%"
if changedKeys == nil
|| changedKeys?.contains(String(keyPath: \Modern.ColorsDemo.Palette.$hue)) == true {
self.hueSlider.value = Float(palette.hue)
}
if changedKeys == nil
|| changedKeys?.contains(String(keyPath: \Modern.ColorsDemo.Palette.$saturation)) == true {
self.saturationSlider.value = palette.saturation
}
if changedKeys == nil
|| changedKeys?.contains(String(keyPath: \Modern.ColorsDemo.Palette.$brightness)) == true {
self.brightnessSlider.value = palette.brightness
}
}
// MARK: ObjectObserver
func objectMonitor(
_ monitor: ObjectMonitor<Modern.ColorsDemo.Palette>,
didUpdateObject object: Modern.ColorsDemo.Palette,
changedPersistentKeys: Set<KeyPathString>,
sourceIdentifier: Any?
) {
self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys)
}
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
let view = self.view!
let containerView = UIView()
do {
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = UIColor.white
containerView.layer.cornerRadius = 10
containerView.layer.masksToBounds = true
containerView.layer.shadowColor = UIColor(white: 0.5, alpha: 0.3).cgColor
containerView.layer.shadowOffset = .init(width: 1, height: 1)
containerView.layer.shadowRadius = 2
view.addSubview(containerView)
}
let vStackView = UIStackView()
do {
vStackView.translatesAutoresizingMaskIntoConstraints = false
vStackView.axis = .vertical
vStackView.spacing = 10
vStackView.distribution = .fill
vStackView.alignment = .fill
containerView.addSubview(vStackView)
}
let palette = self.palette.object
let rows: [(label: UILabel, slider: UISlider, initialValue: Float, sliderValueChangedSelector: Selector)] = [
(
self.hueLabel,
self.hueSlider,
palette?.hue ?? 0,
#selector(self.hueSliderValueDidChange(_:))
),
(
self.saturationLabel,
self.saturationSlider,
palette?.saturation ?? 0,
#selector(self.saturationSliderValueDidChange(_:))
),
(
self.brightnessLabel,
self.brightnessSlider,
palette?.brightness ?? 0,
#selector(self.brightnessSliderValueDidChange(_:))
)
]
for (label, slider, initialValue, sliderValueChangedSelector) in rows {
let hStackView = UIStackView()
do {
hStackView.translatesAutoresizingMaskIntoConstraints = false
hStackView.axis = .horizontal
hStackView.spacing = 5
hStackView.distribution = .fill
hStackView.alignment = .center
vStackView.addArrangedSubview(hStackView)
}
do {
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = UIColor(white: 0, alpha: 0.8)
label.textAlignment = .center
hStackView.addArrangedSubview(label)
}
do {
slider.translatesAutoresizingMaskIntoConstraints = false
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = initialValue
slider.addTarget(
self,
action: sliderValueChangedSelector,
for: .valueChanged
)
hStackView.addArrangedSubview(slider)
}
}
layout: do {
NSLayoutConstraint.activate(
[
containerView.leadingAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.leadingAnchor,
constant: 10
),
containerView.centerYAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.centerYAnchor
),
containerView.trailingAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.trailingAnchor,
constant: -10
),
vStackView.topAnchor.constraint(
equalTo: containerView.topAnchor,
constant: 15
),
vStackView.leadingAnchor.constraint(
equalTo: containerView.leadingAnchor,
constant: 15
),
vStackView.bottomAnchor.constraint(
equalTo: containerView.bottomAnchor,
constant: -15
),
vStackView.trailingAnchor.constraint(
equalTo: containerView.trailingAnchor,
constant: -15
)
]
)
NSLayoutConstraint.activate(
rows.map { label, _, _, _ in
label.widthAnchor.constraint(equalToConstant: 80)
}
)
}
self.startMonitoringObject()
}
// MARK: Private
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError()
}
private let hueLabel: UILabel = .init()
private let saturationLabel: UILabel = .init()
private let brightnessLabel: UILabel = .init()
private let hueSlider: UISlider = .init()
private let saturationSlider: UISlider = .init()
private let brightnessSlider: UISlider = .init()
@objc
private dynamic func hueSliderValueDidChange(_ sender: UISlider) {
let value = sender.value
Modern.ColorsDemo.dataStack.perform(
asynchronous: { [weak self] (transaction) in
let palette = transaction.edit(self?.palette.object)
palette?.hue = value
},
completion: { _ in }
)
}
@objc
private dynamic func saturationSliderValueDidChange(_ sender: UISlider) {
let value = sender.value
Modern.ColorsDemo.dataStack.perform(
asynchronous: { [weak self] (transaction) in
let palette = transaction.edit(self?.palette.object)
palette?.saturation = value
},
completion: { _ in }
)
}
@objc
private dynamic func brightnessSliderValueDidChange(_ sender: UISlider) {
let value = sender.value
Modern.ColorsDemo.dataStack.perform(
asynchronous: { [weak self] (transaction) in
let palette = transaction.edit(self?.palette.object)
palette?.brightness = value
},
completion: { _ in }
)
}
}
}

View File

@@ -0,0 +1,28 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Modern.ColorsDemo.UIKit
extension Modern.ColorsDemo.UIKit {
// MARK: - Modern.ColorsDemo.UIKit.ItemCell
final class ItemCell: UITableViewCell {
// MARK: Internal
static let reuseIdentifier: String = NSStringFromClass(Modern.ColorsDemo.UIKit.ItemCell.self)
func setPalette(_ palette: Modern.ColorsDemo.Palette) {
self.contentView.backgroundColor = palette.color
self.textLabel?.text = palette.colorText
self.textLabel?.textColor = palette.brightness > 0.6 ? .black : .white
}
}
}

View File

@@ -0,0 +1,89 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import SwiftUI
// MARK: - Modern.ColorsDemo.UIKit
extension Modern.ColorsDemo.UIKit {
// MARK: - Modern.ColorsDemo.UIKit.ListView
struct ListView: UIViewControllerRepresentable {
// MARK: Internal
init(
listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
onPaletteTapped: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
) {
self.listPublisher = listPublisher
self.onPaletteTapped = onPaletteTapped
}
// MARK: UIViewControllerRepresentable
typealias UIViewControllerType = Modern.ColorsDemo.UIKit.ListViewController
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
return UIViewControllerType(
listPublisher: self.listPublisher,
onPaletteTapped: self.onPaletteTapped
)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
uiViewController.setEditing(
context.environment.editMode?.wrappedValue.isEditing == true,
animated: true
)
}
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
// MARK: Private
private let listPublisher: ListPublisher<Modern.ColorsDemo.Palette>
private let onPaletteTapped: (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
}
}
#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
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()
}
}
)
return Modern.ColorsDemo.UIKit.ListView(
listPublisher: Modern.ColorsDemo.palettesPublisher,
onPaletteTapped: { _ in }
)
}
}
#endif

View File

@@ -0,0 +1,157 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Modern.ColorsDemo.UIKit
extension Modern.ColorsDemo.UIKit {
// MARK: - Modern.ColorsDemo.UIKit.ListViewController
final class ListViewController: UITableViewController {
/**
Sample 1: Setting up a `DiffableDataSource.TableViewAdapter` that will manage tableView snapshot updates automatically. We can use the built-in `DiffableDataSource.TableViewAdapter` type directly, but in our case we want to enabled `UITableView` cell deletions so we create a custom subclass `CustomDataSource` (see declaration below).
*/
private lazy var dataSource: DiffableDataSource.TableViewAdapter<Modern.ColorsDemo.Palette> = CustomDataSource(
tableView: self.tableView,
dataStack: Modern.ColorsDemo.dataStack,
cellProvider: { (tableView, indexPath, palette) in
let cell = tableView.dequeueReusableCell(
withIdentifier: Modern.ColorsDemo.UIKit.ItemCell.reuseIdentifier,
for: indexPath
) as! Modern.ColorsDemo.UIKit.ItemCell
cell.setPalette(palette)
return cell
}
)
/**
Sample 2: Once the views are created, we can start binding `ListPublisher` updates to the `DiffableDataSource`. We typically call this at the end of `viewDidLoad`. Note that the `addObserver`'s closure argument will only be called on the succeeding updates, so to immediately display the current values, we need to call `dataSource.apply()` once. This example inspects the optional `transactionSource` to determine the source of the update which is helpful for debugging or for fine-tuning animations.
*/
private func startObservingList() {
let dataSource = self.dataSource
self.listPublisher.addObserver(self, notifyInitial: true) { (listPublisher, transactionSource) in
switch transactionSource as? Modern.ColorsDemo.TransactionSource {
case .add,
.delete,
.shuffle,
.clear:
dataSource.apply(listPublisher.snapshot, animatingDifferences: true)
case nil,
.refetch:
dataSource.apply(listPublisher.snapshot, animatingDifferences: false)
}
}
}
/**
Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ListPublisher`s safely remove deallocated observers automatically.
*/
deinit {
self.listPublisher.removeObserver(self)
}
/**
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 {
return
}
self.dataStack.perform(
asynchronous: { (transaction) in
transaction.delete(objectIDs: [itemID])
},
sourceIdentifier: Modern.ColorsDemo.TransactionSource.delete,
completion: { _ in }
)
default:
break
}
}
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return self.sectionIndexTitlesForAllSections().compactMap({ $0 })
}
override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
return index
}
}
// MARK: Internal
init(
listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
onPaletteTapped: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
) {
self.listPublisher = listPublisher
self.onPaletteTapped = onPaletteTapped
super.init(style: .plain)
}
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(
Modern.ColorsDemo.UIKit.ItemCell.self,
forCellReuseIdentifier: Modern.ColorsDemo.UIKit.ItemCell.reuseIdentifier
)
self.startObservingList()
}
// MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.onPaletteTapped(
self.listPublisher.snapshot[indexPath]
)
}
// MARK: Private
private let listPublisher: ListPublisher<Modern.ColorsDemo.Palette>
private let onPaletteTapped: (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError()
}
}
}

View File

@@ -0,0 +1,13 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
// MARK: - Modern.ColorsDemo
extension Modern.ColorsDemo {
// MARK: - UIKit
enum UIKit {}
}

View File

@@ -0,0 +1,86 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import SwiftUI
import CoreStore
// MARK: - Modern
extension Modern {
// MARK: - Modern.ColorsDemo
/**
Sample usages for observing lists or single instances of `CoreStoreObject`s
*/
enum ColorsDemo {
// MARK: Internal
static let dataStack: DataStack = {
let dataStack = DataStack(
CoreStoreSchema(
modelVersion: "V1",
entities: [
Entity<Modern.ColorsDemo.Palette>("Palette")
],
versionLock: [
"Palette": [0xbaf4eaee9353176a, 0xdd6ca918cc2b0c38, 0xd04fad8882d7cc34, 0x3e90ca38c091503f]
]
)
)
/**
- Important: `addStorageAndWait(_:)` was used here to simplify initializing the demo, but in practice the asynchronous function variants are recommended.
*/
try! dataStack.addStorageAndWait(
SQLiteStore(
fileName: "Modern.ColorsDemo.sqlite",
localStorageOptions: .recreateStoreOnModelMismatch
)
)
return dataStack
}()
static let palettesPublisher: ListPublisher<Modern.ColorsDemo.Palette> = Modern.ColorsDemo.dataStack.publishList(
From<Modern.ColorsDemo.Palette>()
.sectionBy(
\.$colorGroup,
sectionIndexTransformer: { $0?.first?.uppercased() }
)
.where(Modern.ColorsDemo.filter.whereClause())
.orderBy(.ascending(\.$hue))
)
static var filter: Modern.ColorsDemo.Filter = .all {
didSet {
try! Modern.ColorsDemo.palettesPublisher.refetch(
From<Modern.ColorsDemo.Palette>()
.sectionBy(
\.$colorGroup,
sectionIndexTransformer: { $0?.first?.uppercased() }
)
.where(self.filter.whereClause())
.orderBy(.ascending(\.$hue)),
sourceIdentifier: TransactionSource.refetch
)
}
}
// MARK: - TransactionSource
enum TransactionSource {
case add
case delete
case shuffle
case clear
case refetch
}
}
}

View File

@@ -0,0 +1,10 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
// MARK: - Modern
/**
Sample usages for `CoreStoreObject` subclasses
*/
enum Modern {}

View File

@@ -0,0 +1,67 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Contacts
import CoreLocation
import CoreStore
// MARK: - Modern.PlacemarksDemo
extension Modern.PlacemarksDemo {
// MARK: Geocoder
final class Geocoder {
// MARK: Internal
func geocode(
place: ObjectSnapshot<Modern.PlacemarksDemo.Place>,
completion: @escaping (_ title: String?, _ subtitle: String?) -> Void
) {
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
)
)
}
)
}
// MARK: Private
private var geocoder: CLGeocoder?
}
}

View File

@@ -0,0 +1,163 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreLocation
import Combine
import CoreStore
import Foundation
import MapKit
import SwiftUI
// MARK: - Modern.PlacemarksDemo
extension Modern.PlacemarksDemo {
// MARK: - Modern.PlacemarksDemo.MainView
struct MainView: View {
/**
Sample 1: Asynchronous transactions
*/
private func demoAsynchronousTransaction(coordinate: CLLocationCoordinate2D) {
Modern.PlacemarksDemo.dataStack.perform(
asynchronous: { (transaction) in
let place = self.$place?.asEditable(in: transaction)
place?.annotation = .init(coordinate: coordinate)
},
completion: { _ in }
)
}
/**
Sample 2: Synchronous transactions
- 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
let place = self.$place?.asEditable(in: transaction)
place?.setRandomLocation()
}
)
}
/**
Sample 3: Unsafe transactions
- Important: `beginUnsafe()` was used here for illustration purposes. In practice, `perform(asynchronous:completion:)` is the preferred transaction type. Use Unsafe Transactions only when you need to bypass CoreStore's serialized transactions.
*/
private func demoUnsafeTransaction(
title: String?,
subtitle: String?,
for snapshot: ObjectSnapshot<Modern.PlacemarksDemo.Place>
) {
let transaction = Modern.PlacemarksDemo.dataStack.beginUnsafe()
let place = snapshot.asEditable(in: transaction)
place?.title = title
place?.subtitle = subtitle
transaction.commit { (error) in
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
var body: some View {
Group {
if let place = self.place {
Modern.PlacemarksDemo.MapView(
place: place,
onTap: { coordinate in
self.demoAsynchronousTransaction(coordinate: coordinate)
}
)
.overlay(
InstructionsView(
("Random", "Sets random coordinate"),
("Tap", "Sets to tapped coordinate")
)
.padding(.leading, 10)
.padding(.bottom, 40),
alignment: .bottomLeading
)
}
}
.navigationBarTitle("Placemarks")
.navigationBarItems(
trailing: Button("Random") {
self.demoSynchronousTransaction()
}
)
}
// MARK: Private
private var sinkCancellable: AnyCancellable?
private let geocoder = Modern.PlacemarksDemo.Geocoder()
}
}
#if DEBUG
struct _Demo_Modern_PlacemarksDemo_MainView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Modern.PlacemarksDemo.MainView()
}
}
#endif

View File

@@ -0,0 +1,119 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreLocation
import CoreStore
import MapKit
import UIKit
import SwiftUI
// MARK: - Modern.PlacemarksDemo
extension Modern.PlacemarksDemo {
// MARK: - Modern.PlacemarksDemo.MapView
struct MapView: UIViewRepresentable {
// MARK: Internal
var place: ObjectSnapshot<Modern.PlacemarksDemo.Place>?
let onTap: (CLLocationCoordinate2D) -> Void
// MARK: UIViewRepresentable
typealias UIViewType = MKMapView
func makeUIView(context: Context) -> UIViewType {
let coordinator = context.coordinator
let mapView = MKMapView()
mapView.delegate = coordinator
mapView.addGestureRecognizer(
UITapGestureRecognizer(
target: coordinator,
action: #selector(coordinator.tapGestureRecognized(_:))
)
)
return mapView
}
func updateUIView(_ view: UIViewType, context: Context) {
let currentAnnotations = view.annotations
view.removeAnnotations(currentAnnotations)
guard let newAnnotation = self.place?.$annotation else {
return
}
view.addAnnotation(newAnnotation)
view.setCenter(newAnnotation.coordinate, animated: true)
view.selectAnnotation(newAnnotation, animated: true)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
// MARK: Internal
init(_ parent: MapView) {
self.parent = parent
}
// MARK: MKMapViewDelegate
@objc dynamic func mapView(
_ mapView: MKMapView,
viewFor annotation: MKAnnotation
) -> MKAnnotationView? {
let identifier = "MKAnnotationView"
var annotationView: MKPinAnnotationView! = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView
if annotationView == nil {
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView.isEnabled = true
annotationView.canShowCallout = true
annotationView.animatesDrop = true
}
else {
annotationView.annotation = annotation
}
return annotationView
}
// MARK: FilePrivate
@objc
fileprivate dynamic func tapGestureRecognized(_ gesture: UILongPressGestureRecognizer) {
guard
case let mapView as MKMapView = gesture.view,
gesture.state == .recognized
else {
return
}
let coordinate = mapView.convert(
gesture.location(in: mapView),
toCoordinateFrom: mapView
)
self.parent.onTap(coordinate)
}
// MARK: Private
private var parent: MapView
}
}
}

View File

@@ -0,0 +1,125 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import struct CoreLocation.CLLocationCoordinate2D
import protocol MapKit.MKAnnotation
// MARK: - Modern.PlacemarksDemo
extension Modern.PlacemarksDemo {
// MARK: - Modern.PlacemarksDemo.Place
final class Place: CoreStoreObject {
// MARK: Internal
@Field.Stored("latitude")
var latitude: Double = 0
@Field.Stored("longitude")
var longitude: Double = 0
@Field.Stored("title")
var title: String?
@Field.Stored("subtitle")
var subtitle: String?
@Field.Virtual(
"annotation",
customGetter: { object, field in
Modern.PlacemarksDemo.Place.Annotation(object)
},
customSetter: { object, field, newValue in
object.$latitude.value = newValue.coordinate.latitude
object.$longitude.value = newValue.coordinate.longitude
object.$title.value = "\(newValue.coordinate.latitude), \(newValue.coordinate.longitude)"
object.$subtitle.value = nil
}
)
var annotation: Modern.PlacemarksDemo.Place.Annotation
func setRandomLocation() {
self.latitude = Double(arc4random_uniform(180)) - 90
self.longitude = Double(arc4random_uniform(360)) - 180
self.title = "\(self.latitude), \(self.longitude)"
self.subtitle = nil
}
// MARK: - Modern.PlacemarksDemo.Place.Annotation
final class Annotation: NSObject, MKAnnotation {
// MARK: Internal
init(coordinate: CLLocationCoordinate2D) {
self.coordinate = coordinate
self.title = nil
self.subtitle = nil
}
// MARK: MKAnnotation
let coordinate: CLLocationCoordinate2D
let title: String?
let subtitle: String?
// MARK: NSObjectProtocol
override func isEqual(_ object: Any?) -> Bool {
guard case let object as Annotation = object else {
return false
}
return self.coordinate.latitude == object.coordinate.latitude
&& self.coordinate.longitude == object.coordinate.longitude
&& self.title == object.title
&& self.subtitle == object.subtitle
}
override var hash: Int {
var hasher = Hasher()
hasher.combine(self.coordinate.latitude)
hasher.combine(self.coordinate.longitude)
hasher.combine(self.title)
hasher.combine(self.subtitle)
return hasher.finalize()
}
// MARK: FilePrivate
fileprivate init(
latitude: Double,
longitude: Double,
title: String?,
subtitle: String?
) {
self.coordinate = .init(latitude: latitude, longitude: longitude)
self.title = title
self.subtitle = subtitle
}
fileprivate init(_ object: ObjectProxy<Modern.PlacemarksDemo.Place>) {
self.coordinate = .init(
latitude: object.$latitude.value,
longitude: object.$longitude.value
)
self.title = object.$title.value
self.subtitle = object.$subtitle.value
}
}
}
}

View File

@@ -0,0 +1,67 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Modern
extension Modern {
// MARK: - Modern.PlacemarksDemo
/**
Sample usages for `CoreStoreObject` transactions
*/
enum PlacemarksDemo {
// MARK: Internal
/**
Sample 1: Setting up the `DataStack` and storage
*/
static let dataStack: DataStack = {
let dataStack = DataStack(
CoreStoreSchema(
modelVersion: "V1",
entities: [
Entity<Modern.PlacemarksDemo.Place>("Place")
],
versionLock: [
"Place": [0xa7eec849af5e8fcb, 0x638e69c040090319, 0x4e976d66ed400447, 0x18e96bc0438d07bb]
]
)
)
/**
- Important: `addStorageAndWait(_:)` and `perform(synchronous:)` methods were used here to simplify initializing the demo, but in practice the asynchronous function variants are recommended.
*/
try! dataStack.addStorageAndWait(
SQLiteStore(
fileName: "Modern.PlacemarksDemo.sqlite",
localStorageOptions: .recreateStoreOnModelMismatch
)
)
return dataStack
}()
static let placePublisher: ObjectPublisher<Modern.PlacemarksDemo.Place> = {
let dataStack = Modern.PlacemarksDemo.dataStack
if let place = try! dataStack.fetchOne(From<Place>()) {
return dataStack.publishObject(place)
}
_ = try! dataStack.perform(
synchronous: { (transaction) in
let place = transaction.create(Into<Place>())
place.setRandomLocation()
}
)
let place = try! dataStack.fetchOne(From<Place>())
return dataStack.publishObject(place!)
}()
}
}

View File

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

View File

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

View File

@@ -0,0 +1,95 @@
//
// 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(
listView: @escaping () -> ListView
) {
self.listView = listView
}
// MARK: View
var body: some View {
ZStack {
self.listView()
.frame(minHeight: 0, maxHeight: .infinity)
.edgesIgnoringSafeArea(.vertical)
if self.pokedexEntries.isEmpty {
VStack(alignment: .center, spacing: 30) {
Text("This demo needs to make a network connection to download Pokedex entries")
.multilineTextAlignment(.center)
if self.service.isLoading {
Text("Fetching Pokedex…")
}
else {
Button(
action: { self.service.fetchPokedexEntries() },
label: {
Text("Download Pokedex Entries")
}
)
}
}
.padding()
}
}
.navigationBarTitle("Pokedex")
}
// MARK: Private
@ListState(
From<Modern.PokedexDemo.PokedexEntry>()
.orderBy(.ascending(\.$index)),
in: Modern.PokedexDemo.dataStack
)
private var pokedexEntries
@ObservedObject
private var service: Modern.PokedexDemo.Service = .init()
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

View File

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

View File

@@ -0,0 +1,65 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.PokemonType
/**
Sample 1: Types that will be used with `@Field.Stored` need to implement both `ImportableAttributeType` and `FieldStorableType`. In this case, `RawRepresentable` types with primitive `RawValue`s have built-in implementations so we only have to declare conformance to `ImportableAttributeType` and `FieldStorableType`.
*/
enum PokemonType: String, CaseIterable, ImportableAttributeType, FieldStorableType {
// MARK: Internal
case bug
case dark
case dragon
case electric
case fairy
case fighting
case fire
case flying
case ghost
case grass
case ground
case ice
case normal
case poison
case psychic
case rock
case steel
case water
var color: UIColor {
switch self {
case .bug: return #colorLiteral(red: 0.568627450980392, green: 0.749019607843137, blue: 0.231372549019608, alpha: 1.0) // #91BF3B, a: 1.0
case .dark: return #colorLiteral(red: 0.392156862745098, green: 0.388235294117647, blue: 0.454901960784314, alpha: 1.0) // #646374, a: 1.0
case .dragon: return #colorLiteral(red: 0.0823529411764706, green: 0.423529411764706, blue: 0.741176470588235, alpha: 1.0) // #156CBD, a: 1.0
case .electric: return #colorLiteral(red: 0.949019607843137, green: 0.819607843137255, blue: 0.298039215686275, alpha: 1.0) // #F2D14C, a: 1.0
case .fairy: return #colorLiteral(red: 0.913725490196078, green: 0.56078431372549, blue: 0.882352941176471, alpha: 1.0) // #E98FE1, a: 1.0
case .fighting: return #colorLiteral(red: 0.8, green: 0.254901960784314, blue: 0.423529411764706, alpha: 1.0) // #CC416C, a: 1.0
case .fire: return #colorLiteral(red: 0.992156862745098, green: 0.607843137254902, blue: 0.352941176470588, alpha: 1.0) // #FD9B5A, a: 1.0
case .flying: return #colorLiteral(red: 0.619607843137255, green: 0.701960784313725, blue: 0.886274509803922, alpha: 1.0) // #9EB3E2, a: 1.0
case .ghost: return #colorLiteral(red: 0.333333333333333, green: 0.419607843137255, blue: 0.670588235294118, alpha: 1.0) // #556BAB, a: 1.0
case .grass: return #colorLiteral(red: 0.38823529411764707, green: 0.7215686274509804, blue: 0.3803921568627451, alpha: 1.0) // #63B861, a: 1.0
case .ground: return #colorLiteral(red: 0.847058823529412, green: 0.458823529411765, blue: 0.298039215686275, alpha: 1.0) // #D8754C, a: 1.0
case .ice: return #colorLiteral(red: 0.466666666666667, green: 0.803921568627451, blue: 0.756862745098039, alpha: 1.0) // #77CDC1, a: 1.0
case .normal: return #colorLiteral(red: 0.564705882352941, green: 0.603921568627451, blue: 0.627450980392157, alpha: 1.0) // #909AA0, a: 1.0
case .poison: return #colorLiteral(red: 0.647058823529412, green: 0.411764705882353, blue: 0.768627450980392, alpha: 1.0) // #A569C4, a: 1.0
case .psychic: return #colorLiteral(red: 0.9764705882, green: 0.5058823529, blue: 0.5019607843, alpha: 1) // #F98180, a: 1.0
case .rock: return #colorLiteral(red: 0.776470588235294, green: 0.717647058823529, blue: 0.556862745098039, alpha: 1.0) // #C6B78E, a: 1.0
case .steel: return #colorLiteral(red: 0.329411764705882, green: 0.529411764705882, blue: 0.607843137254902, alpha: 1.0) // #54879B, a: 1.0
case .water: return #colorLiteral(red: 0.325490196078431, green: 0.576470588235294, blue: 0.823529411764706, alpha: 1.0) // #5393D2, a: 1.0
}
}
}
}

View File

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

View File

@@ -0,0 +1,139 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.Species
/**
Sample 1: This sample shows how to declare `CoreStoreObject` subclasses that implement `ImportableUniqueObject`. For this class the `ImportSource` is a JSON `Dictionary`.
*/
final class Species: CoreStoreObject, ImportableUniqueObject {
// MARK: Internal
@Field.Stored("id")
var id: Int = 0
@Field.Stored("name")
var name: String = ""
@Field.Stored("weight")
var weight: Int = 0
@Field.Stored("pokemonType1")
var pokemonType1: Modern.PokedexDemo.PokemonType = .normal
@Field.Stored("pokemonType2")
var pokemonType2: Modern.PokedexDemo.PokemonType?
@Field.Stored("statHitPoints")
var statHitPoints: Int = 0
@Field.Stored("statAttack")
var statAttack: Int = 0
@Field.Stored("statDefense")
var statDefense: Int = 0
@Field.Stored("statSpecialAttack")
var statSpecialAttack: Int = 0
@Field.Stored("statSpecialDefense")
var statSpecialDefense: Int = 0
@Field.Stored("statSpeed")
var statSpeed: Int = 0
@Field.Coded(
"formsURLs",
coder: FieldCoders.Json.self
)
var formsURLs: [URL] = []
@Field.Relationship("details", inverse: \.$species)
var details: Modern.PokedexDemo.Details?
// MARK: ImportableObject
typealias ImportSource = Dictionary<String, Any>
// MARK: ImportableUniqueObject
typealias UniqueIDType = Int
static let uniqueIDKeyPath: String = String(keyPath: \Modern.PokedexDemo.Species.$id)
var uniqueIDValue: UniqueIDType {
get { return self.id }
set { self.id = newValue }
}
static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType? {
let json = source
return try Modern.PokedexDemo.Service.parseJSON(json["id"])
}
func update(from source: ImportSource, in transaction: BaseDataTransaction) throws {
typealias Service = Modern.PokedexDemo.Service
let json = source
self.name = try Service.parseJSON(json["name"])
self.weight = try Service.parseJSON(json["weight"])
for json in try Service.parseJSON(json["types"]) as [Dictionary<String, Any>] {
let slot: Int = try Service.parseJSON(json["slot"])
let pokemonType = try Service.parseJSON(
json["type"],
transformer: { (json: Dictionary<String, Any>) in
Modern.PokedexDemo.PokemonType(rawValue: try Service.parseJSON(json["name"]))
}
)
switch slot {
case 1: self.pokemonType1 = pokemonType
case 2: self.pokemonType2 = pokemonType
default: continue
}
}
for json in try Service.parseJSON(json["stats"]) as [Dictionary<String, Any>] {
let baseStat: Int = try Service.parseJSON(json["base_stat"])
let name: String = try Service.parseJSON(
json["stat"],
transformer: { (json: Dictionary<String, Any>) in
try Service.parseJSON(json["name"])
}
)
switch name {
case "hp": self.statHitPoints = baseStat
case "attack": self.statAttack = baseStat
case "defense": self.statDefense = baseStat
case "special-attack": self.statSpecialAttack = baseStat
case "special-defense": self.statSpecialDefense = baseStat
case "speed": self.statSpeed = baseStat
default: continue
}
}
self.formsURLs = try (Service.parseJSON(json["forms"]) as [Dictionary<String, Any>])
.map({ try Service.parseJSON($0["url"], transformer: URL.init(string:)) })
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - UIKit
enum UIKit {}
}

View File

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

View File

@@ -0,0 +1,58 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import SwiftUI
// MARK: - Modern.TimeZonesDemo
extension Modern.TimeZonesDemo {
// MARK: - Modern.TimeZonesDemo.ItemView
struct ItemView: View {
// 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)
}
}
// MARK: FilePrivate
fileprivate let title: String
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

View File

@@ -0,0 +1,94 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import SwiftUI
// MARK: - Modern.TimeZonesDemo
extension Modern.TimeZonesDemo {
// MARK: - Modern.TimeZonesDemo.ListView
struct ListView: View {
// MARK: Internal
init(title: String, objects: [Modern.TimeZonesDemo.TimeZone]) {
self.title = title
self.values = objects.map {
(title: $0.name, subtitle: $0.abbreviation)
}
}
init(title: String, value: Any?) {
self.title = title
switch value {
case (let array as [Any])?:
self.values = array.map {
(
title: String(describing: $0),
subtitle: String(reflecting: type(of: $0))
)
}
case let item?:
self.values = [
(
title: String(describing: item),
subtitle: String(reflecting: type(of: item))
)
]
case nil:
self.values = []
}
}
// 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)
}
// MARK: Private
private let title: String
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

View File

@@ -0,0 +1,234 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import SwiftUI
// MARK: - Modern.TimeZonesDemo
extension Modern.TimeZonesDemo {
// MARK: - Modern.TimeZonesDemo.MainView
struct MainView: View {
/**
Sample 1: Plain object fetch
*/
private func fetchAllTimeZones() -> [Modern.TimeZonesDemo.TimeZone] {
return try! Modern.TimeZonesDemo.dataStack.fetchAll(
From<Modern.TimeZonesDemo.TimeZone>()
.orderBy(.ascending(\.$secondsFromGMT))
)
}
/**
Sample 2: Plain object fetch with simple `where` clause
*/
private func fetchTimeZonesWithDST() -> [Modern.TimeZonesDemo.TimeZone] {
return try! Modern.TimeZonesDemo.dataStack.fetchAll(
From<Modern.TimeZonesDemo.TimeZone>()
.where(\.$isDaylightSavingTime == true)
.orderBy(.ascending(\.$name))
)
}
/**
Sample 3: Plain object fetch with custom `where` clause
*/
private func fetchTimeZonesInAsia() -> [Modern.TimeZonesDemo.TimeZone] {
return try! Modern.TimeZonesDemo.dataStack.fetchAll(
From<Modern.TimeZonesDemo.TimeZone>()
.where(
format: "%K BEGINSWITH[c] %@",
String(keyPath: \Modern.TimeZonesDemo.TimeZone.$name),
"Asia"
)
.orderBy(.ascending(\.$secondsFromGMT))
)
}
/**
Sample 4: Plain object fetch with complex `where` clauses
*/
private func fetchTimeZonesNearUTC() -> [Modern.TimeZonesDemo.TimeZone] {
let secondsIn3Hours = 60 * 60 * 3
return try! Modern.TimeZonesDemo.dataStack.fetchAll(
From<Modern.TimeZonesDemo.TimeZone>()
.where((-secondsIn3Hours ... secondsIn3Hours) ~= \.$secondsFromGMT)
/// equivalent to:
/// ```
/// .where(\.$secondsFromGMT >= -secondsIn3Hours
/// && \.$secondsFromGMT <= secondsIn3Hours)
/// ```
.orderBy(.ascending(\.$secondsFromGMT))
)
}
/**
Sample 5: Querying single raw value with simple `select` clause
*/
private func queryNumberOfTimeZones() -> Int? {
return try! Modern.TimeZonesDemo.dataStack.queryValue(
From<Modern.TimeZonesDemo.TimeZone>()
.select(Int.self, .count(\.$name))
)
}
/**
Sample 6: Querying single raw values with `select` and `where` clauses
*/
private func queryTokyoTimeZoneAbbreviation() -> String? {
return try! Modern.TimeZonesDemo.dataStack.queryValue(
From<Modern.TimeZonesDemo.TimeZone>()
.select(String.self, .attribute(\.$abbreviation))
.where(
format: "%K ENDSWITH[c] %@",
String(keyPath: \Modern.TimeZonesDemo.TimeZone.$name),
"Tokyo"
)
)
}
/**
Sample 7: Querying a list of raw values with multiple attributes
*/
private func queryAllNamesAndAbbreviations() -> [[String: Any]]? {
return try! Modern.TimeZonesDemo.dataStack.queryAttributes(
From<Modern.TimeZonesDemo.TimeZone>()
.select(
NSDictionary.self,
.attribute(\.$name),
.attribute(\.$abbreviation)
)
.orderBy(.ascending(\.$name))
)
}
/**
Sample 7: Querying a list of raw values grouped by similar field
*/
private func queryNumberOfCountriesWithAndWithoutDST() -> [[String: Any]]? {
return try! Modern.TimeZonesDemo.dataStack.queryAttributes(
From<Modern.TimeZonesDemo.TimeZone>()
.select(
NSDictionary.self,
.count(\.$isDaylightSavingTime, as: "numberOfCountries"),
.attribute(\.$isDaylightSavingTime)
)
.groupBy(\.$isDaylightSavingTime)
.orderBy(
.ascending(\.$isDaylightSavingTime),
.ascending(\.$name)
)
)
}
// MARK: View
var body: some View {
List {
Section(header: Text("Fetching objects")) {
ForEach(self.fetchingItems, id: \.title) { item in
Menu.ItemView(
title: item.title,
destination: {
Modern.TimeZonesDemo.ListView(
title: item.title,
objects: item.objects()
)
}
)
}
}
Section(header: Text("Querying raw values")) {
ForEach(self.queryingItems, id: \.title) { item in
Menu.ItemView(
title: item.title,
destination: {
Modern.TimeZonesDemo.ListView(
title: item.title,
value: item.value()
)
}
)
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Time Zones")
}
// MARK: Private
private var fetchingItems: [(title: String, objects: () -> [Modern.TimeZonesDemo.TimeZone])] {
return [
(
"All Time Zones",
self.fetchAllTimeZones
),
(
"Time Zones with Daylight Savings",
self.fetchTimeZonesWithDST
),
(
"Time Zones in Asia",
self.fetchTimeZonesInAsia
),
(
"Time Zones at most 3 hours away from UTC",
self.fetchTimeZonesNearUTC
)
]
}
private var queryingItems: [(title: String, value: () -> Any?)] {
return [
(
"Number of Time Zones",
self.queryNumberOfTimeZones
),
(
"Abbreviation for Tokyo's Time Zone",
self.queryTokyoTimeZoneAbbreviation
),
(
"All Names and Abbreviations",
self.queryAllNamesAndAbbreviations
),
(
"Number of Countries with and without DST",
self.queryNumberOfCountriesWithAndWithoutDST
)
]
}
}
}
#if DEBUG
struct _Demo_Modern_TimeZonesDemo_MainView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Modern.TimeZonesDemo.MainView()
}
}
#endif

View File

@@ -0,0 +1,32 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Modern.TimeZonesDemo
extension Modern.TimeZonesDemo {
// MARK: - Modern.TimeZonesDemo.TimeZone
final class TimeZone: CoreStoreObject {
// MARK: Internal
@Field.Stored("secondsFromGMT")
var secondsFromGMT: Int = 0
@Field.Stored("abbreviation")
var abbreviation: String = ""
@Field.Stored("isDaylightSavingTime")
var isDaylightSavingTime: Bool = false
@Field.Stored("daylightSavingTimeOffset")
var daylightSavingTimeOffset: Double = 0
@Field.Stored("name")
var name: String = ""
}
}

View File

@@ -0,0 +1,64 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Modern
extension Modern {
// MARK: - Modern.TimeZonesDemo
/**
Sample usages for creating Fetch and Query clauses for `CoreStoreObject`s
*/
enum TimeZonesDemo {
// MARK: Internal
static let dataStack: DataStack = {
let dataStack = DataStack(
CoreStoreSchema(
modelVersion: "V1",
entities: [
Entity<Modern.TimeZonesDemo.TimeZone>("TimeZone")
],
versionLock: [
"TimeZone": [0x9b1d35108434c8fd, 0x4cb8a80903e66b64, 0x405acca3c1945fe3, 0x3b49dccaee0753d8]
]
)
)
/**
- Important: `addStorageAndWait(_:)` and `perform(synchronous:)` methods were used here to simplify initializing the demo, but in practice the asynchronous function variants are recommended.
*/
try! dataStack.addStorageAndWait(
SQLiteStore(
fileName: "Modern.TimeZonesDemo.sqlite",
localStorageOptions: .recreateStoreOnModelMismatch
)
)
_ = try! dataStack.perform(
synchronous: { (transaction) in
try transaction.deleteAll(From<TimeZone>())
for name in NSTimeZone.knownTimeZoneNames {
let rawTimeZone = NSTimeZone(name: name)!
let cachedTimeZone = transaction.create(Into<TimeZone>())
cachedTimeZone.name = rawTimeZone.name
cachedTimeZone.abbreviation = rawTimeZone.abbreviation ?? ""
cachedTimeZone.secondsFromGMT = rawTimeZone.secondsFromGMT
cachedTimeZone.isDaylightSavingTime = rawTimeZone.isDaylightSavingTime
cachedTimeZone.daylightSavingTimeOffset = rawTimeZone.daylightSavingTimeOffset
}
}
)
return dataStack
}()
}
}

View File

@@ -0,0 +1,69 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Foundation
import UIKit
import Combine
// MARK: - ImageDownloader
final class ImageDownloader: ObservableObject {
// MARK: FilePrivate
private(set) var image: UIImage?
let url: URL?
init(url: URL?) {
self.url = url
guard let url = url else {
return
}
if let image = Self.cache[url] {
self.image = image
}
}
func fetchImage(completion: @escaping (UIImage) -> Void = { _ in }) {
guard let url = url else {
return
}
if let image = Self.cache[url] {
self.objectWillChange.send()
self.image = image
completion(image)
return
}
self.cancellable = URLSession.shared
.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in },
receiveValue: { output in
if let image = UIImage(data: output.data) {
Self.cache[url] = image
self.objectWillChange.send()
self.image = image
completion(image)
}
}
)
}
// MARK: Private
private static var cache: [URL: UIImage] = [:]
private var cancellable: AnyCancellable?
}

View File

@@ -0,0 +1,58 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import SwiftUI
// MARK: - InstructionsView
struct InstructionsView: View {
// MARK: Internal
init(_ rows: (header: String, description: String)...) {
self.rows = rows.map({ .init(header: $0, description: $1) })
}
// MARK: View
var body: some View {
ZStack(alignment: .center) {
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: 3) {
ForEach(self.rows, id: \.header) { row in
HStack(alignment: .firstTextBaseline, spacing: 5) {
Text(row.header)
.font(.callout)
.fontWeight(.bold)
Text(row.description)
.font(.footnote)
}
}
}
.foregroundColor(Color(.sRGB, white: 0, opacity: 0.8))
.padding(.horizontal, 10)
.padding(.vertical, 4)
}
.fixedSize()
}
// MARK: Private
private let rows: [InstructionsView.Row]
// MARK: - Row
struct Row: Hashable {
// MARK: Internal
let header: String
let description: String
}
}

View File

@@ -0,0 +1,29 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import SwiftUI
// MARK: - LazyView
struct LazyView<Content: View>: View {
// MARK: Internal
init(_ load: @escaping () -> Content) {
self.load = load
}
// MARK: View
var body: Content {
self.load()
}
// MARK: Private
private let load: () -> Content
}

View File

@@ -0,0 +1,71 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import SwiftUI
// MARK: - Menu
extension Menu {
// MARK: - Menu.ItemView
struct ItemView<Destination: View>: View {
// MARK: Internal
init(
title: String,
subtitle: String? = nil,
destination: @escaping () -> Destination
) {
self.title = title
self.subtitle = subtitle
self.destination = destination
}
// MARK: View
var body: some View {
NavigationLink(destination: LazyView(self.destination)) {
VStack(alignment: .leading) {
Text(self.title)
.font(.headline)
.foregroundColor(.primary)
self.subtitle.map {
Text($0)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
// MARK: FilePrivate
fileprivate let title: String
fileprivate let subtitle: String?
fileprivate let destination: () -> Destination
}
}
#if DEBUG
struct _Demo_Menu_ItemView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Menu.ItemView(
title: "Item Title",
subtitle: "A subtitle caption for this item",
destination: {
Color.blue
}
)
}
}
#endif

View File

@@ -0,0 +1,134 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Foundation
import SwiftUI
// MARK: - Menu
extension Menu {
// MARK: - Menu.MainView
struct MainView: View {
// MARK: View
var body: some View {
NavigationView {
List {
Section(header: Text("Modern (CoreStoreObject subclasses)")) {
Menu.ItemView(
title: "Placemarks",
subtitle: "Making changes using Transactions",
destination: {
Modern.PlacemarksDemo.MainView()
}
)
Menu.ItemView(
title: "Time Zones",
subtitle: "Fetching objects and Querying raw values",
destination: {
Modern.TimeZonesDemo.MainView()
}
)
Menu.ItemView(
title: "Colors (UIKit)",
subtitle: "Observing list changes and single-object changes using DiffableDataSources",
destination: {
Modern.ColorsDemo.MainView(
listView: { listPublisher, onPaletteTapped in
Modern.ColorsDemo.UIKit.ListView(
listPublisher: listPublisher,
onPaletteTapped: onPaletteTapped
)
.edgesIgnoringSafeArea(.all)
},
detailView: { objectPublisher in
Modern.ColorsDemo.UIKit.DetailView(objectPublisher)
}
)
}
)
Menu.ItemView(
title: "Colors (SwiftUI)",
subtitle: "Observing list changes and single-object changes using SwiftUI bindings",
destination: {
Modern.ColorsDemo.MainView(
listView: { listPublisher, onPaletteTapped in
Modern.ColorsDemo.SwiftUI.ListView(
listPublisher: listPublisher,
onPaletteTapped: onPaletteTapped
)
},
detailView: { objectPublisher in
Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher)
}
)
}
)
Menu.ItemView(
title: "Pokedex API",
subtitle: "Importing JSON data from external source",
destination: {
Modern.PokedexDemo.MainView(
listView: Modern.PokedexDemo.UIKit.ListView.init
)
}
)
}
Section(header: Text("Classic (NSManagedObject subclasses)")) {
Menu.ItemView(
title: "Colors",
subtitle: "Observing list changes and single-object changes using ListMonitor",
destination: {
Classic.ColorsDemo.MainView()
}
)
}
Section(header: Text("Advanced")) {
Menu.ItemView(
title: "Accounts",
subtitle: "Switching between multiple persistent stores",
destination: { EmptyView() }
)
.disabled(true)
Menu.ItemView(
title: "Evolution",
subtitle: "Migrating and reverse-migrating stores",
destination: {
Advanced.EvolutionDemo.MainView()
}
)
Menu.ItemView(
title: "Logger",
subtitle: "Implementing a custom logger",
destination: { EmptyView() }
)
.disabled(true)
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("CoreStore Demos")
Menu.PlaceholderView()
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
}
#if DEBUG
struct _Demo_Menu_MainView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Menu.MainView()
}
}
#endif

View File

@@ -0,0 +1,44 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Combine
import CoreStore
import SwiftUI
// MARK: - Menu
extension Menu {
// MARK: - Menu.PlaceholderView
struct PlaceholderView: UIViewControllerRepresentable {
// MARK: UIViewControllerRepresentable
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
return UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()!
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {}
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
}
}
#if DEBUG
struct _Demo_Menu_PlaceholderView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
return Menu.PlaceholderView()
}
}
#endif

View File

@@ -0,0 +1,10 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Foundation
// MARK: - Menu
enum Menu {}

View File

@@ -0,0 +1,36 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import SwiftUI
import UIKit
// MARK: - SceneDelegate
@objc final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// MARK: UIWindowSceneDelegate
@objc dynamic var window: UIWindow?
// MARK: UISceneDelegate
@objc dynamic func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard case let scene as UIWindowScene = scene else {
return
}
let window = UIWindow(windowScene: scene)
window.rootViewController = UIHostingController(
rootView: Menu.MainView()
)
self.window = window
window.makeKeyAndVisible()
}
}