mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-03-19 16:21:27 +01:00
Xcode 14, iOS 16 SDK (min iOS 13)
This commit is contained in:
33
Demo/Sources/AppDelegate.swift
Normal file
33
Demo/Sources/AppDelegate.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
10
Demo/Sources/Demos/Advanced/Advanced.swift
Normal file
10
Demo/Sources/Demos/Advanced/Advanced.swift
Normal 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 {}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(_:_:)
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
10
Demo/Sources/Demos/Classic/Classic.swift
Normal file
10
Demo/Sources/Demos/Classic/Classic.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
// MARK: - Classic
|
||||
|
||||
/**
|
||||
Sample usages for `NSManagedObject` subclasses
|
||||
*/
|
||||
enum Classic {}
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
|
||||
// MARK: - Modern.ColorsDemo
|
||||
|
||||
extension Modern.ColorsDemo {
|
||||
|
||||
// MARK: - SwiftUI
|
||||
|
||||
enum SwiftUI {}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
|
||||
// MARK: - Modern.ColorsDemo
|
||||
|
||||
extension Modern.ColorsDemo {
|
||||
|
||||
// MARK: - UIKit
|
||||
|
||||
enum UIKit {}
|
||||
}
|
||||
86
Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift
Normal file
86
Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Demo/Sources/Demos/Modern/Modern.swift
Normal file
10
Demo/Sources/Demos/Modern/Modern.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
// MARK: - Modern
|
||||
|
||||
/**
|
||||
Sample usages for `CoreStoreObject` subclasses
|
||||
*/
|
||||
enum Modern {}
|
||||
@@ -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?
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!)
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
@@ -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:))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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:))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:)) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
extension Modern.PokedexDemo {
|
||||
|
||||
// MARK: - UIKit
|
||||
|
||||
enum UIKit {}
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = ""
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}()
|
||||
}
|
||||
}
|
||||
69
Demo/Sources/Helpers/ImageDownloader.swift
Normal file
69
Demo/Sources/Helpers/ImageDownloader.swift
Normal 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?
|
||||
}
|
||||
58
Demo/Sources/Helpers/InstructionsView.swift
Normal file
58
Demo/Sources/Helpers/InstructionsView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
29
Demo/Sources/Helpers/LazyView.swift
Normal file
29
Demo/Sources/Helpers/LazyView.swift
Normal 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
|
||||
}
|
||||
71
Demo/Sources/Helpers/Menu/Menu.ItemView.swift
Normal file
71
Demo/Sources/Helpers/Menu/Menu.ItemView.swift
Normal 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
|
||||
134
Demo/Sources/Helpers/Menu/Menu.MainView.swift
Normal file
134
Demo/Sources/Helpers/Menu/Menu.MainView.swift
Normal 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
|
||||
44
Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift
Normal file
44
Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift
Normal 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
|
||||
10
Demo/Sources/Helpers/Menu/Menu.swift
Normal file
10
Demo/Sources/Helpers/Menu/Menu.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
// MARK: - Menu
|
||||
|
||||
enum Menu {}
|
||||
36
Demo/Sources/SceneDelegate.swift
Normal file
36
Demo/Sources/SceneDelegate.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user