mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-03-21 08:59:24 +01:00
added demo for classic ListMonitor
This commit is contained in:
@@ -1,33 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
// MARK: - Classic
|
||||
|
||||
/**
|
||||
Sample usages for `NSManagedObject` subclasses
|
||||
*/
|
||||
enum Classic {}
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// 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 (\Modern.ColorsDemo.Palette.$brightness >= 0.9)
|
||||
case .dark: return (\Modern.ColorsDemo.Palette.$brightness <= 0.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
//
|
||||
// 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(
|
||||
"colorName",
|
||||
customGetter: { object, field in
|
||||
|
||||
if let colorName = field.primitiveValue {
|
||||
|
||||
return colorName
|
||||
}
|
||||
let colorName: String
|
||||
switch object.$hue.value * 359 {
|
||||
|
||||
case 0 ..< 20: colorName = "Lower Reds"
|
||||
case 20 ..< 57: colorName = "Oranges and Browns"
|
||||
case 57 ..< 90: colorName = "Yellow-Greens"
|
||||
case 90 ..< 159: colorName = "Greens"
|
||||
case 159 ..< 197: colorName = "Blue-Greens"
|
||||
case 197 ..< 241: colorName = "Blues"
|
||||
case 241 ..< 297: colorName = "Violets"
|
||||
case 297 ..< 331: colorName = "Magentas"
|
||||
default: colorName = "Upper Reds"
|
||||
}
|
||||
field.primitiveValue = colorName
|
||||
return colorName
|
||||
}
|
||||
)
|
||||
var colorName: 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.$colorName.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.0 ... 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
|
||||
// MARK: - Modern.ColorsDemo
|
||||
|
||||
extension Modern.ColorsDemo {
|
||||
|
||||
// MARK: - SwiftUI
|
||||
|
||||
enum SwiftUI {}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
//
|
||||
// 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) {}
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let 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
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
|
||||
// MARK: - Modern.ColorsDemo
|
||||
|
||||
extension Modern.ColorsDemo {
|
||||
|
||||
// MARK: - UIKit
|
||||
|
||||
enum UIKit {}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
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(\.$colorName)
|
||||
.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(\.$colorName)
|
||||
.where(self.filter.whereClause())
|
||||
.orderBy(.ascending(\.$hue))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
//
|
||||
// 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 {
|
||||
|
||||
/**
|
||||
⭐️ Sample 1: Setting a sectioned `ListPublisher` declared as an `@ObservedObject`
|
||||
*/
|
||||
@ObservedObject
|
||||
private var listPublisher: ListPublisher<Modern.ColorsDemo.Palette>
|
||||
|
||||
// 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
|
||||
self.listPublisher = Modern.ColorsDemo.palettesPublisher
|
||||
self._filter = Binding(
|
||||
get: { Modern.ColorsDemo.filter },
|
||||
set: { Modern.ColorsDemo.filter = $0 }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
let detailView: AnyView
|
||||
if let selectedPalette = self.selectedPalette {
|
||||
|
||||
detailView = AnyView(
|
||||
self.detailView(selectedPalette)
|
||||
)
|
||||
}
|
||||
else {
|
||||
|
||||
detailView = AnyView(EmptyView())
|
||||
}
|
||||
let listPublisher = self.listPublisher
|
||||
return VStack(spacing: 0) {
|
||||
self.listView(listPublisher, { self.selectedPalette = $0 })
|
||||
.navigationBarTitle(
|
||||
Text("Colors (\(listPublisher.snapshot.numberOfItems) objects)")
|
||||
)
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
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 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>?
|
||||
|
||||
@Binding
|
||||
private var filter: Modern.ColorsDemo.Filter
|
||||
|
||||
private func changeFilter() {
|
||||
|
||||
Modern.ColorsDemo.filter = Modern.ColorsDemo.filter.next()
|
||||
}
|
||||
|
||||
private func clearColors() {
|
||||
|
||||
Modern.ColorsDemo.dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
try transaction.deleteAll(From<Modern.ColorsDemo.Palette>())
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
private func addColor() {
|
||||
|
||||
Modern.ColorsDemo.dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
_ = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
private func shuffleColors() {
|
||||
|
||||
Modern.ColorsDemo.dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
for palette in try transaction.fetchAll(From<Modern.ColorsDemo.Palette>()) {
|
||||
|
||||
palette.setRandomHue()
|
||||
}
|
||||
},
|
||||
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
|
||||
@@ -1,166 +0,0 @@
|
||||
//
|
||||
// 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: Setting an `ObjectPublisher` declared as an `@ObservedObject`
|
||||
*/
|
||||
@ObservedObject
|
||||
private var palette: ObjectPublisher<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 = 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 {
|
||||
|
||||
guard let snapshot = self.palette.snapshot else {
|
||||
|
||||
return AnyView(EmptyView())
|
||||
}
|
||||
return AnyView(
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .bottom) {
|
||||
Color(snapshot.$color)
|
||||
ZStack {
|
||||
Color.white
|
||||
.cornerRadius(10)
|
||||
.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(snapshot.$hue * 359))°")
|
||||
.frame(width: 80)
|
||||
Slider(
|
||||
value: self.$hue,
|
||||
in: 0 ... 1,
|
||||
step: 1 / 359
|
||||
)
|
||||
}
|
||||
HStack {
|
||||
Text("S: \(Int(snapshot.$saturation * 100))%")
|
||||
.frame(width: 80)
|
||||
Slider(
|
||||
value: self.$saturation,
|
||||
in: 0 ... 1,
|
||||
step: 1 / 100
|
||||
)
|
||||
}
|
||||
HStack {
|
||||
Text("B: \(Int(snapshot.$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()
|
||||
.padding(geometry.safeAreaInsets)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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
|
||||
@@ -1,76 +0,0 @@
|
||||
//
|
||||
// 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: Setting an `ObjectPublisher` declared as an `@ObservedObject`
|
||||
*/
|
||||
@ObservedObject
|
||||
private var palette: ObjectPublisher<Modern.ColorsDemo.Palette>
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
internal init(_ palette: ObjectPublisher<Modern.ColorsDemo.Palette>) {
|
||||
|
||||
self.palette = palette
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
|
||||
guard let palette = self.palette.snapshot else {
|
||||
|
||||
return AnyView(EmptyView())
|
||||
}
|
||||
return AnyView(
|
||||
Color(palette.$color).overlay(
|
||||
Text(palette.$colorText)
|
||||
.foregroundColor(palette.$brightness > 0.6 ? .black : .white)
|
||||
.padding(),
|
||||
alignment: .leading
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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
|
||||
@@ -1,119 +0,0 @@
|
||||
//
|
||||
// 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: Setting a sectioned `ListPublisher` declared as an `@ObservedObject`
|
||||
*/
|
||||
@ObservedObject
|
||||
private var listPublisher: ListPublisher<Modern.ColorsDemo.Palette>
|
||||
|
||||
/**
|
||||
⭐️ Sample 2: Assigning sections and items of the `ListPublisher` to corresponding `View`s
|
||||
*/
|
||||
var body: some View {
|
||||
let listSnapshot = self.listPublisher.snapshot
|
||||
return List {
|
||||
ForEach(listSnapshot.sectionIDs, id: \.self) { (sectionID) in
|
||||
Section(header: Text(sectionID)) {
|
||||
ForEach(listSnapshot.items(inSectionWithID: sectionID), id: \.self) { palette in
|
||||
Button(
|
||||
action: {
|
||||
self.onPaletteTapped(palette)
|
||||
},
|
||||
label: {
|
||||
Modern.ColorsDemo.SwiftUI.ItemView(palette)
|
||||
}
|
||||
)
|
||||
.listRowInsets(.init())
|
||||
}
|
||||
.onDelete { itemIndices in
|
||||
|
||||
self.deleteColors(at: itemIndices, in: sectionID)
|
||||
}
|
||||
}
|
||||
}
|
||||
GeometryReader { geometry in
|
||||
Spacer(minLength: geometry.safeAreaInsets.bottom)
|
||||
}
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
}
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(
|
||||
listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
|
||||
onPaletteTapped: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
|
||||
) {
|
||||
|
||||
self.listPublisher = listPublisher
|
||||
self.onPaletteTapped = onPaletteTapped
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let onPaletteTapped: (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
|
||||
|
||||
private func deleteColors(at indices: IndexSet, in sectionID: String) {
|
||||
|
||||
let objectIDsToDelete = self.listPublisher.snapshot.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
|
||||
@@ -1,285 +0,0 @@
|
||||
//
|
||||
// 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.
|
||||
*/
|
||||
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>
|
||||
) {
|
||||
|
||||
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
|
||||
|
||||
private let palette: ObjectMonitor<Modern.ColorsDemo.Palette>
|
||||
|
||||
@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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
//
|
||||
// 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 `DeletionEnabledDataSource` (see declaration below).
|
||||
*/
|
||||
private lazy var dataSource: DiffableDataSource.TableViewAdapter<Modern.ColorsDemo.Palette> = DeletionEnabledDataSource(
|
||||
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.
|
||||
*/
|
||||
private func startObservingList() {
|
||||
|
||||
self.listPublisher.addObserver(self) { (listPublisher) in
|
||||
|
||||
self.dataSource.apply(
|
||||
listPublisher.snapshot,
|
||||
animatingDifferences: true
|
||||
)
|
||||
}
|
||||
self.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 on the `UITableView`.
|
||||
*/
|
||||
final class DeletionEnabledDataSource: DiffableDataSource.TableViewAdapter<Modern.ColorsDemo.Palette> {
|
||||
|
||||
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])
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
// MARK: - Modern
|
||||
|
||||
/**
|
||||
Sample usages for `CoreStoreObject` subclasses
|
||||
*/
|
||||
enum Modern {}
|
||||
@@ -1,67 +0,0 @@
|
||||
//
|
||||
// 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?
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
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!)
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
@ObservedObject
|
||||
var place: ObjectPublisher<Modern.PlacemarksDemo.Place>
|
||||
|
||||
init() {
|
||||
|
||||
self.place = Modern.PlacemarksDemo.placePublisher
|
||||
self.sinkCancellable = self.place.sink(
|
||||
receiveCompletion: { _ in
|
||||
|
||||
// Deleted, do nothing
|
||||
},
|
||||
receiveValue: { [self] (snapshot) in
|
||||
|
||||
self.geocoder.geocode(place: snapshot) { (title, subtitle) in
|
||||
|
||||
guard self.place.snapshot == snapshot else {
|
||||
|
||||
return
|
||||
}
|
||||
self.demoUnsafeTransaction(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
for: snapshot
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
Modern.PlacemarksDemo.MapView(
|
||||
place: self.place.snapshot,
|
||||
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? = nil
|
||||
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
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// 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]
|
||||
}
|
||||
}
|
||||
@@ -1,387 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import CoreStore
|
||||
import UIKit
|
||||
|
||||
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
extension Modern.PokedexDemo {
|
||||
|
||||
// MARK: - Modern.PokedexDemo.ItemCell
|
||||
|
||||
final class ItemCell: UICollectionViewCell {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static let reuseIdentifier: String = NSStringFromClass(Modern.PokedexDemo.ItemCell.self)
|
||||
|
||||
func setPokedexEntry(
|
||||
_ pokedexEntry: Modern.PokedexDemo.PokedexEntry,
|
||||
service: Modern.PokedexDemo.Service
|
||||
) {
|
||||
|
||||
guard let pokedexEntry = pokedexEntry.asPublisher() else {
|
||||
|
||||
return self.pokedexEntry = nil
|
||||
}
|
||||
guard self.pokedexEntry != pokedexEntry else {
|
||||
|
||||
return
|
||||
}
|
||||
self.service = service
|
||||
self.pokedexEntry = pokedexEntry
|
||||
|
||||
self.didUpdateData(animated: false)
|
||||
|
||||
if let snapshot = pokedexEntry.snapshot {
|
||||
|
||||
service.fetchDetails(for: snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
|
||||
fatalError()
|
||||
}
|
||||
|
||||
|
||||
// MARK: UITableViewCell
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
let contentView = self.contentView
|
||||
do {
|
||||
|
||||
contentView.backgroundColor = UIColor.placeholderText.withAlphaComponent(0.1)
|
||||
contentView.layer.cornerRadius = 10
|
||||
contentView.layer.masksToBounds = true
|
||||
}
|
||||
|
||||
let typesContainerView = UIStackView()
|
||||
do {
|
||||
|
||||
typesContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
typesContainerView.axis = .horizontal
|
||||
typesContainerView.alignment = .fill
|
||||
typesContainerView.distribution = .fillEqually
|
||||
typesContainerView.spacing = 0
|
||||
|
||||
typesContainerView.addArrangedSubview(self.type1View)
|
||||
typesContainerView.addArrangedSubview(self.type2View)
|
||||
|
||||
contentView.addSubview(typesContainerView)
|
||||
}
|
||||
|
||||
let spriteView = self.spriteView
|
||||
do {
|
||||
|
||||
spriteView.translatesAutoresizingMaskIntoConstraints = false
|
||||
spriteView.contentMode = .scaleAspectFill
|
||||
|
||||
contentView.addSubview(spriteView)
|
||||
}
|
||||
let placeholderLabel = self.placeholderLabel
|
||||
do {
|
||||
|
||||
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
placeholderLabel.textColor = UIColor.placeholderText
|
||||
placeholderLabel.font = UIFont.systemFont(ofSize: 20, weight: .heavy)
|
||||
placeholderLabel.numberOfLines = 0
|
||||
placeholderLabel.textAlignment = .center
|
||||
|
||||
contentView.addSubview(placeholderLabel)
|
||||
}
|
||||
let nameLabel = self.nameLabel
|
||||
do {
|
||||
|
||||
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
nameLabel.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
extension Modern.PokedexDemo {
|
||||
|
||||
// MARK: - Modern.PokedexDemo.ListView
|
||||
|
||||
struct ListView: UIViewControllerRepresentable {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(
|
||||
service: Modern.PokedexDemo.Service,
|
||||
listPublisher: ListPublisher<Modern.PokedexDemo.PokedexEntry>
|
||||
) {
|
||||
|
||||
self.service = service
|
||||
self.listPublisher = listPublisher
|
||||
}
|
||||
|
||||
|
||||
// MARK: UIViewControllerRepresentable
|
||||
|
||||
typealias UIViewControllerType = Modern.PokedexDemo.ListViewController
|
||||
|
||||
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
|
||||
|
||||
return UIViewControllerType(
|
||||
service: self.service,
|
||||
listPublisher: self.listPublisher
|
||||
)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {}
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@ObservedObject
|
||||
private var service: Modern.PokedexDemo.Service
|
||||
|
||||
private let listPublisher: ListPublisher<Modern.PokedexDemo.PokedexEntry>
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_PokedexDemo_ListView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let service = Modern.PokedexDemo.Service()
|
||||
service.fetchPokedexEntries()
|
||||
|
||||
return Modern.PokedexDemo.ListView(
|
||||
service: service,
|
||||
listPublisher: Modern.PokedexDemo.pokedexEntries
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,111 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
import UIKit
|
||||
|
||||
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
extension Modern.PokedexDemo {
|
||||
|
||||
// MARK: - Modern.PokedexDemo.ListViewController
|
||||
|
||||
final class ListViewController: UICollectionViewController {
|
||||
|
||||
// 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.ItemCell.self,
|
||||
forCellWithReuseIdentifier: Modern.PokedexDemo.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.ItemCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as! Modern.PokedexDemo.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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
//
|
||||
// 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: View {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init() {
|
||||
|
||||
self.pokedexEntries = Modern.PokedexDemo.pokedexEntries
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
let pokedexEntries = self.pokedexEntries.snapshot
|
||||
return ZStack {
|
||||
|
||||
Modern.PokedexDemo.ListView(
|
||||
service: self.service,
|
||||
listPublisher: self.pokedexEntries
|
||||
)
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
.edgesIgnoringSafeArea(.vertical)
|
||||
|
||||
if pokedexEntries.isEmpty {
|
||||
|
||||
VStack(alignment: .center, spacing: 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
|
||||
|
||||
@ObservedObject
|
||||
private var pokedexEntries: ListPublisher<Modern.PokedexDemo.PokedexEntry>
|
||||
|
||||
@ObservedObject
|
||||
private var service: Modern.PokedexDemo.Service = .init()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct _Demo_Modern_PokedexDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Modern.PokedexDemo.MainView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,57 +0,0 @@
|
||||
//
|
||||
// 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))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// 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:))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
//
|
||||
// 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:))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
import UIKit
|
||||
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
extension Modern.PokedexDemo {
|
||||
|
||||
// MARK: - Modern.PokedexDemo.Move
|
||||
|
||||
/**
|
||||
⭐️ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
//
|
||||
// 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:)) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
@@ -1,94 +0,0 @@
|
||||
//
|
||||
// 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),
|
||||
dsubtitleetail: 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
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// 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 = ""
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// ImageDownloader.swift
|
||||
// Demo
|
||||
//
|
||||
// Created by John Rommel Estropia on 2020/08/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
// MARK: - ImageDownloader
|
||||
|
||||
final class ImageDownloader: ObservableObject {
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
private(set) var image: UIImage?
|
||||
|
||||
let url: URL?
|
||||
|
||||
init(url: URL?) {
|
||||
|
||||
self.url = url
|
||||
guard let url = url else {
|
||||
|
||||
return
|
||||
}
|
||||
if let image = Self.cache[url] {
|
||||
|
||||
self.image = image
|
||||
}
|
||||
}
|
||||
|
||||
func fetchImage(completion: @escaping (UIImage) -> Void = { _ in }) {
|
||||
|
||||
guard let url = url else {
|
||||
|
||||
return
|
||||
}
|
||||
if let image = Self.cache[url] {
|
||||
|
||||
self.objectWillChange.send()
|
||||
self.image = image
|
||||
completion(image)
|
||||
return
|
||||
}
|
||||
self.cancellable = URLSession.shared
|
||||
.dataTaskPublisher(for: url)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { output in
|
||||
|
||||
if let image = UIImage(data: output.data) {
|
||||
|
||||
Self.cache[url] = image
|
||||
self.objectWillChange.send()
|
||||
self.image = image
|
||||
completion(image)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private static var cache: [URL: UIImage] = [:]
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// 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) {
|
||||
Color.white
|
||||
.cornerRadius(10)
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
@@ -1,151 +0,0 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
)
|
||||
}
|
||||
Section(header: Text("Classic (NSManagedObject subclasses)")) {
|
||||
Menu.ItemView(
|
||||
title: "Placemarks (Swift)",
|
||||
subtitle: "Making changes using transactions in Swift",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Placemarks (Objective-C)",
|
||||
subtitle: "Making changes using transactions in Objective-C",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Time Zones",
|
||||
subtitle: "Fetching objects and Querying raw values",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Colors (Swift)",
|
||||
subtitle: "Observing list changes and single-object changes in Swift",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Colors (Objective-C)",
|
||||
subtitle: "Observing list changes and single-object changes in Objective-C",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Pokedex API",
|
||||
subtitle: "Importing JSON data from external source",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
}
|
||||
Section(header: Text("Advanced")) {
|
||||
Menu.ItemView(
|
||||
title: "Accounts",
|
||||
subtitle: "Switching between multiple persistent stores",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Evolution",
|
||||
subtitle: "Migrating and reverse-migrating stores",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Logger",
|
||||
subtitle: "Implementing a custom logger",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
}
|
||||
}
|
||||
.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
|
||||
@@ -1,44 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
@@ -1,10 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
// MARK: - Menu
|
||||
|
||||
enum Menu {}
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// 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