mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-09-10 03:41:47 +02:00
WIP
This commit is contained in:
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
extension Modern.ColorsDemo {
|
||||
|
||||
// MARK: - Modern.ColorsDemo.MainView
|
||||
|
||||
|
||||
struct MainView<ListView: View, DetailView: View>: View {
|
||||
|
||||
// MARK: Internal
|
||||
@@ -20,7 +20,8 @@ extension Modern.ColorsDemo {
|
||||
_ listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
|
||||
_ onPaletteTapped: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
|
||||
) -> ListView,
|
||||
detailView: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> DetailView) {
|
||||
detailView: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> DetailView
|
||||
) {
|
||||
|
||||
self.listView = listView
|
||||
self.detailView = detailView
|
||||
@@ -30,42 +31,51 @@ extension Modern.ColorsDemo {
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
return VStack(spacing: 0) {
|
||||
self.listView(self.$palettes, { self.selectedPalette = $0 })
|
||||
.navigationBarTitle(
|
||||
Text("Colors (\(self.palettes.count) objects)")
|
||||
)
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
self.selectedPalette.map {
|
||||
|
||||
VStack(spacing: 0) {
|
||||
|
||||
self.listView(
|
||||
self.$palettes,
|
||||
{
|
||||
self.selectedPalette = $0
|
||||
}
|
||||
)
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
|
||||
if let selectedPalette = self.selectedPalette {
|
||||
|
||||
self.detailView($0)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
self.detailView(selectedPalette)
|
||||
.ignoresSafeArea()
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
.navigationBarItems(
|
||||
leading: HStack {
|
||||
.navigationTitle("Colors (\(self.palettes.count) objects)")
|
||||
.toolbar {
|
||||
|
||||
ToolbarItemGroup(placement: .topBarLeading) {
|
||||
EditButton()
|
||||
Button(
|
||||
action: { self.clearColors() },
|
||||
label: { Text("Clear") }
|
||||
)
|
||||
},
|
||||
trailing: HStack {
|
||||
Button(
|
||||
action: { self.changeFilter() },
|
||||
label: { Text(self.filter.rawValue) }
|
||||
)
|
||||
Button(
|
||||
action: { self.shuffleColors() },
|
||||
label: { Text("Shuffle") }
|
||||
)
|
||||
Button(
|
||||
action: { self.addColor() },
|
||||
label: { Text("Add") }
|
||||
)
|
||||
Button("Clear") {
|
||||
|
||||
self.clearColors()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||
|
||||
Button(self.filter.rawValue) {
|
||||
|
||||
self.changeFilter()
|
||||
}
|
||||
Button("Shuffle") {
|
||||
|
||||
self.shuffleColors()
|
||||
}
|
||||
Button("Add") {
|
||||
|
||||
self.addColor()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +109,7 @@ extension Modern.ColorsDemo {
|
||||
|
||||
Modern.ColorsDemo.dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
|
||||
try transaction.deleteAll(From<Modern.ColorsDemo.Palette>())
|
||||
},
|
||||
sourceIdentifier: TransactionSource.clear,
|
||||
@@ -108,10 +118,10 @@ extension Modern.ColorsDemo {
|
||||
}
|
||||
|
||||
private func addColor() {
|
||||
|
||||
|
||||
Modern.ColorsDemo.dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
|
||||
_ = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
},
|
||||
sourceIdentifier: TransactionSource.add,
|
||||
@@ -120,12 +130,12 @@ extension Modern.ColorsDemo {
|
||||
}
|
||||
|
||||
private func shuffleColors() {
|
||||
|
||||
|
||||
Modern.ColorsDemo.dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
|
||||
for palette in try transaction.fetchAll(From<Modern.ColorsDemo.Palette>()) {
|
||||
|
||||
|
||||
palette.setRandomHue()
|
||||
}
|
||||
},
|
||||
@@ -136,42 +146,38 @@ extension Modern.ColorsDemo {
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_ColorsDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
// MARK: - Preview
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
#Preview {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
)
|
||||
return Modern.ColorsDemo.MainView(
|
||||
listView: { listPublisher, onPaletteTapped in
|
||||
Modern.ColorsDemo.SwiftUI.ListView(
|
||||
listPublisher: listPublisher,
|
||||
onPaletteTapped: onPaletteTapped
|
||||
)
|
||||
},
|
||||
detailView: { objectPublisher in
|
||||
Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher)
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
return Modern.ColorsDemo.MainView(
|
||||
listView: { listPublisher, onPaletteTapped in
|
||||
|
||||
Modern.ColorsDemo.SwiftUI.ListView(
|
||||
listPublisher: listPublisher,
|
||||
onPaletteTapped: onPaletteTapped
|
||||
)
|
||||
},
|
||||
detailView: { objectPublisher in
|
||||
|
||||
Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
|
||||
@@ -31,9 +30,9 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
|
||||
@Binding
|
||||
private var brightness: Float
|
||||
|
||||
|
||||
init(_ palette: ObjectPublisher<Modern.ColorsDemo.Palette>) {
|
||||
|
||||
|
||||
self._palette = .init(palette)
|
||||
self._hue = Binding(
|
||||
get: { palette.hue ?? 0 },
|
||||
@@ -78,7 +77,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
@@ -87,12 +86,17 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
if let palette = self.palette {
|
||||
|
||||
ZStack(alignment: .center) {
|
||||
|
||||
Color(palette.$color)
|
||||
|
||||
ZStack {
|
||||
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(Color.white)
|
||||
.shadow(color: Color(.sRGB, white: 0.5, opacity: 0.3), radius: 2, x: 1, y: 1)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
|
||||
HStack {
|
||||
Text("H: \(Int(palette.$hue * 359))°")
|
||||
.frame(width: 80)
|
||||
@@ -102,6 +106,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
step: 1 / 359
|
||||
)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("S: \(Int(palette.$saturation * 100))%")
|
||||
.frame(width: 80)
|
||||
@@ -111,6 +116,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
step: 1 / 100
|
||||
)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("B: \(Int(palette.$brightness * 100))%")
|
||||
.frame(width: 80)
|
||||
@@ -131,30 +137,3 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_ColorsDemo_SwiftUI_DetailView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
guard (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>())) <= 0 else {
|
||||
return
|
||||
}
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
)
|
||||
|
||||
return Modern.ColorsDemo.SwiftUI.DetailView(
|
||||
Modern.ColorsDemo.palettesPublisher.snapshot.first!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -34,41 +34,15 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
|
||||
if let palette = self.palette {
|
||||
|
||||
Color(palette.$color).overlay(
|
||||
Text(palette.$colorText)
|
||||
.foregroundColor(palette.$brightness > 0.6 ? .black : .white)
|
||||
.padding(),
|
||||
alignment: .leading
|
||||
)
|
||||
.animation(.default, value: palette)
|
||||
Color(palette.$color)
|
||||
.overlay(
|
||||
Text(palette.$colorText)
|
||||
.foregroundColor(palette.$brightness > 0.6 ? .black : .white)
|
||||
.padding(),
|
||||
alignment: .leading
|
||||
)
|
||||
.animation(.default, value: palette)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_ColorsDemo_SwiftUI_ItemView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
guard (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>())) <= 0 else {
|
||||
return
|
||||
}
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
)
|
||||
|
||||
return Modern.ColorsDemo.SwiftUI.ItemView(
|
||||
Modern.ColorsDemo.palettesPublisher.snapshot.first!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
extension Modern.ColorsDemo.SwiftUI {
|
||||
|
||||
// MARK: - Modern.ColorsDemo.SwiftUI.ListView
|
||||
|
||||
|
||||
struct ListView: View {
|
||||
|
||||
/**
|
||||
@@ -40,7 +40,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
|
||||
ForEach(sectionIn: self.palettes) { section in
|
||||
|
||||
Section(header: Text(section.sectionID)) {
|
||||
Section(section.sectionID) {
|
||||
|
||||
ForEach(objectIn: section) { palette in
|
||||
|
||||
@@ -64,8 +64,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
}
|
||||
}
|
||||
// .animation(.default) // breaks layout
|
||||
.listStyle(PlainListStyle())
|
||||
.edgesIgnoringSafeArea([])
|
||||
.listStyle(.plain)
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +80,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
)
|
||||
Modern.ColorsDemo.dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
|
||||
transaction.delete(objectIDs: objectIDsToDelete)
|
||||
},
|
||||
completion: { _ in }
|
||||
@@ -90,35 +89,29 @@ extension Modern.ColorsDemo.SwiftUI {
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_ColorsDemo_SwiftUI_ListView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
// MARK: - Preview
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
#Preview {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
)
|
||||
return Modern.ColorsDemo.SwiftUI.ListView(
|
||||
listPublisher: Modern.ColorsDemo.palettesPublisher,
|
||||
onPaletteTapped: { _ in }
|
||||
)
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
}
|
||||
)
|
||||
return Modern.ColorsDemo.SwiftUI.ListView(
|
||||
listPublisher: Modern.ColorsDemo.palettesPublisher,
|
||||
onPaletteTapped: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
|
||||
@@ -22,21 +21,21 @@ extension Modern.ColorsDemo.UIKit {
|
||||
}
|
||||
|
||||
// MARK: UIViewControllerRepresentable
|
||||
|
||||
|
||||
typealias UIViewControllerType = Modern.ColorsDemo.UIKit.DetailViewController
|
||||
|
||||
|
||||
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
|
||||
|
||||
return UIViewControllerType(self.palette)
|
||||
}
|
||||
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
|
||||
|
||||
uiViewController.palette = Modern.ColorsDemo.dataStack.monitorObject(
|
||||
self.palette.object!
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||
|
||||
|
||||
@@ -45,30 +44,3 @@ extension Modern.ColorsDemo.UIKit {
|
||||
private var palette: ObjectPublisher<Modern.ColorsDemo.Palette>
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_ColorsDemo_UIKit_DetailView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
guard (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>())) <= 0 else {
|
||||
return
|
||||
}
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
)
|
||||
|
||||
return Modern.ColorsDemo.UIKit.DetailView(
|
||||
Modern.ColorsDemo.palettesPublisher.snapshot.first!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
+10
-4
@@ -49,7 +49,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
isolated deinit {
|
||||
|
||||
self.palette.removeObserver(self)
|
||||
}
|
||||
@@ -88,14 +88,17 @@ extension Modern.ColorsDemo.UIKit {
|
||||
|
||||
// MARK: ObjectObserver
|
||||
|
||||
func objectMonitor(
|
||||
nonisolated func objectMonitor(
|
||||
_ monitor: ObjectMonitor<Modern.ColorsDemo.Palette>,
|
||||
didUpdateObject object: Modern.ColorsDemo.Palette,
|
||||
didUpdateObject object: sending Modern.ColorsDemo.Palette,
|
||||
changedPersistentKeys: Set<KeyPathString>,
|
||||
sourceIdentifier: Any?
|
||||
) {
|
||||
|
||||
self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys)
|
||||
MainActor.assumeIsolated {
|
||||
|
||||
self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +248,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
private let saturationSlider: UISlider = .init()
|
||||
private let brightnessSlider: UISlider = .init()
|
||||
|
||||
@MainActor
|
||||
@objc
|
||||
private dynamic func hueSliderValueDidChange(_ sender: UISlider) {
|
||||
|
||||
@@ -259,6 +263,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@objc
|
||||
private dynamic func saturationSliderValueDidChange(_ sender: UISlider) {
|
||||
|
||||
@@ -273,6 +278,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@objc
|
||||
private dynamic func brightnessSliderValueDidChange(_ sender: UISlider) {
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
extension Modern.ColorsDemo.UIKit {
|
||||
|
||||
// MARK: - Modern.ColorsDemo.UIKit.ListView
|
||||
|
||||
|
||||
struct ListView: UIViewControllerRepresentable {
|
||||
|
||||
// MARK: Internal
|
||||
@@ -26,9 +26,9 @@ extension Modern.ColorsDemo.UIKit {
|
||||
|
||||
|
||||
// MARK: UIViewControllerRepresentable
|
||||
|
||||
|
||||
typealias UIViewControllerType = Modern.ColorsDemo.UIKit.ListViewController
|
||||
|
||||
|
||||
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
|
||||
|
||||
return UIViewControllerType(
|
||||
@@ -36,7 +36,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
onPaletteTapped: self.onPaletteTapped
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
|
||||
|
||||
uiViewController.setEditing(
|
||||
@@ -44,7 +44,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||
|
||||
|
||||
@@ -55,35 +55,29 @@ extension Modern.ColorsDemo.UIKit {
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_ColorsDemo_UIKit_ListView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
// MARK: - Preview
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
#Preview {
|
||||
|
||||
let minimumSamples = 10
|
||||
try! Modern.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
let missing = minimumSamples
|
||||
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
|
||||
guard missing > 0 else {
|
||||
return
|
||||
}
|
||||
)
|
||||
return Modern.ColorsDemo.UIKit.ListView(
|
||||
listPublisher: Modern.ColorsDemo.palettesPublisher,
|
||||
onPaletteTapped: { _ in }
|
||||
)
|
||||
}
|
||||
for _ in 0..<missing {
|
||||
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
}
|
||||
)
|
||||
return Modern.ColorsDemo.UIKit.ListView(
|
||||
listPublisher: Modern.ColorsDemo.palettesPublisher,
|
||||
onPaletteTapped: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
+14
-14
@@ -42,13 +42,13 @@ extension Modern.ColorsDemo.UIKit {
|
||||
switch transactionSource as? Modern.ColorsDemo.TransactionSource {
|
||||
|
||||
case .add,
|
||||
.delete,
|
||||
.shuffle,
|
||||
.clear:
|
||||
.delete,
|
||||
.shuffle,
|
||||
.clear:
|
||||
dataSource.apply(listPublisher.snapshot, animatingDifferences: true)
|
||||
|
||||
case nil,
|
||||
.refetch:
|
||||
.refetch:
|
||||
dataSource.apply(listPublisher.snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
@@ -57,22 +57,22 @@ extension Modern.ColorsDemo.UIKit {
|
||||
/**
|
||||
⭐️ Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ListPublisher`s safely remove deallocated observers automatically.
|
||||
*/
|
||||
deinit {
|
||||
isolated deinit {
|
||||
|
||||
self.listPublisher.removeObserver(self)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
⭐️ Sample 4: This is the custom `DiffableDataSource.TableViewAdapter` subclass we wrote that enabled swipe-to-delete gestures and section index titles on the `UITableView`.
|
||||
*/
|
||||
final class CustomDataSource: DiffableDataSource.TableViewAdapter<Modern.ColorsDemo.Palette> {
|
||||
|
||||
// MARK: UITableViewDataSource
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
|
||||
|
||||
|
||||
switch editingStyle {
|
||||
|
||||
|
||||
case .delete:
|
||||
guard let itemID = self.itemID(for: indexPath) else {
|
||||
|
||||
@@ -80,13 +80,13 @@ extension Modern.ColorsDemo.UIKit {
|
||||
}
|
||||
self.dataStack.perform(
|
||||
asynchronous: { (transaction) in
|
||||
|
||||
|
||||
transaction.delete(objectIDs: [itemID])
|
||||
},
|
||||
sourceIdentifier: Modern.ColorsDemo.TransactionSource.delete,
|
||||
completion: { _ in }
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -116,10 +116,10 @@ extension Modern.ColorsDemo.UIKit {
|
||||
|
||||
super.init(style: .plain)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: UIViewController
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
super.viewDidLoad()
|
||||
@@ -128,7 +128,7 @@ extension Modern.ColorsDemo.UIKit {
|
||||
Modern.ColorsDemo.UIKit.ItemCell.self,
|
||||
forCellReuseIdentifier: Modern.ColorsDemo.UIKit.ItemCell.reuseIdentifier
|
||||
)
|
||||
|
||||
|
||||
self.startObservingList()
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ extension Modern {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
@MainActor
|
||||
static let dataStack: DataStack = {
|
||||
|
||||
let dataStack = DataStack(
|
||||
@@ -44,6 +45,7 @@ extension Modern {
|
||||
return dataStack
|
||||
}()
|
||||
|
||||
@MainActor
|
||||
static let palettesPublisher: ListPublisher<Modern.ColorsDemo.Palette> = Modern.ColorsDemo.dataStack.publishList(
|
||||
From<Modern.ColorsDemo.Palette>()
|
||||
.sectionBy(
|
||||
@@ -54,6 +56,7 @@ extension Modern {
|
||||
.orderBy(.ascending(\.$hue))
|
||||
)
|
||||
|
||||
@MainActor
|
||||
static var filter: Modern.ColorsDemo.Filter = .all {
|
||||
|
||||
didSet {
|
||||
|
||||
@@ -13,51 +13,58 @@ extension Modern.PlacemarksDemo {
|
||||
|
||||
// MARK: Geocoder
|
||||
|
||||
@MainActor
|
||||
final class Geocoder {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
func geocode(
|
||||
place: ObjectSnapshot<Modern.PlacemarksDemo.Place>,
|
||||
completion: @escaping (_ title: String?, _ subtitle: String?) -> Void
|
||||
) {
|
||||
place: ObjectSnapshot<Modern.PlacemarksDemo.Place>
|
||||
) async -> (title: String?, subtitle: String?) {
|
||||
|
||||
self.geocoder?.cancelGeocode()
|
||||
|
||||
let geocoder = CLGeocoder()
|
||||
self.geocoder = geocoder
|
||||
geocoder.reverseGeocodeLocation(
|
||||
CLLocation(latitude: place.$latitude, longitude: place.$longitude),
|
||||
completionHandler: { (placemarks, error) -> Void in
|
||||
|
||||
defer {
|
||||
|
||||
self.geocoder = nil
|
||||
}
|
||||
guard let placemark = placemarks?.first else {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let address = CNMutablePostalAddress()
|
||||
address.street = placemark.thoroughfare ?? ""
|
||||
address.subLocality = placemark.subThoroughfare ?? ""
|
||||
address.city = placemark.locality ?? ""
|
||||
address.subAdministrativeArea = placemark.subAdministrativeArea ?? ""
|
||||
address.state = placemark.administrativeArea ?? ""
|
||||
address.postalCode = placemark.postalCode ?? ""
|
||||
address.country = placemark.country ?? ""
|
||||
address.isoCountryCode = placemark.isoCountryCode ?? ""
|
||||
|
||||
completion(
|
||||
placemark.name,
|
||||
CNPostalAddressFormatter.string(
|
||||
from: address,
|
||||
style: .mailingAddress
|
||||
)
|
||||
)
|
||||
|
||||
defer {
|
||||
|
||||
if self.geocoder === geocoder {
|
||||
self.geocoder = nil
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
do {
|
||||
let placemarks = try await geocoder.reverseGeocodeLocation(
|
||||
CLLocation(latitude: place.$latitude, longitude: place.$longitude)
|
||||
)
|
||||
guard let placemark = placemarks.first else {
|
||||
|
||||
return (nil, nil)
|
||||
}
|
||||
|
||||
let address = CNMutablePostalAddress()
|
||||
address.street = placemark.thoroughfare ?? ""
|
||||
address.subLocality = placemark.subThoroughfare ?? ""
|
||||
address.city = placemark.locality ?? ""
|
||||
address.subAdministrativeArea = placemark.subAdministrativeArea ?? ""
|
||||
address.state = placemark.administrativeArea ?? ""
|
||||
address.postalCode = placemark.postalCode ?? ""
|
||||
address.country = placemark.country ?? ""
|
||||
address.isoCountryCode = placemark.isoCountryCode ?? ""
|
||||
|
||||
return (
|
||||
placemark.name,
|
||||
CNPostalAddressFormatter.string(
|
||||
from: address,
|
||||
style: .mailingAddress
|
||||
)
|
||||
)
|
||||
}
|
||||
catch {
|
||||
|
||||
return (nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreLocation
|
||||
import Combine
|
||||
import CoreStore
|
||||
import Foundation
|
||||
import MapKit
|
||||
@@ -14,7 +13,7 @@ import SwiftUI
|
||||
extension Modern.PlacemarksDemo {
|
||||
|
||||
// MARK: - Modern.PlacemarksDemo.MainView
|
||||
|
||||
|
||||
struct MainView: View {
|
||||
|
||||
/**
|
||||
@@ -38,7 +37,7 @@ extension Modern.PlacemarksDemo {
|
||||
- Important: `perform(synchronous:)` was used here for illustration purposes. In practice, `perform(asynchronous:completion:)` is the preferred transaction type as synchronous transactions are very likely to cause deadlocks.
|
||||
*/
|
||||
private func demoSynchronousTransaction() {
|
||||
|
||||
|
||||
_ = try? Modern.PlacemarksDemo.dataStack.perform(
|
||||
synchronous: { (transaction) in
|
||||
|
||||
@@ -68,41 +67,12 @@ extension Modern.PlacemarksDemo {
|
||||
print("Commit failed: \(error as Any)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
|
||||
@ObjectState(Modern.PlacemarksDemo.placePublisher)
|
||||
var place: ObjectSnapshot<Modern.PlacemarksDemo.Place>?
|
||||
|
||||
init() {
|
||||
|
||||
self.sinkCancellable = self.$place?.reactive.snapshot().sink(
|
||||
receiveCompletion: { _ in
|
||||
|
||||
// Deleted, do nothing
|
||||
},
|
||||
receiveValue: { [self] (snapshot) in
|
||||
|
||||
guard let snapshot = snapshot else {
|
||||
|
||||
return
|
||||
}
|
||||
self.geocoder.geocode(place: snapshot) { (title, subtitle) in
|
||||
|
||||
guard self.place == snapshot else {
|
||||
|
||||
return
|
||||
}
|
||||
self.demoUnsafeTransaction(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
for: snapshot
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
@@ -130,34 +100,41 @@ extension Modern.PlacemarksDemo {
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Placemarks")
|
||||
.navigationBarItems(
|
||||
trailing: Button("Random") {
|
||||
.task(id: self.place.map({ "\($0.$latitude),\($0.$longitude)" })) {
|
||||
|
||||
guard let place = self.place else {
|
||||
|
||||
return
|
||||
}
|
||||
let geocoded = await self.geocoder.geocode(place: place)
|
||||
guard self.place?.objectID() == place.objectID() else {
|
||||
|
||||
return
|
||||
}
|
||||
guard geocoded.title != nil || geocoded.subtitle != nil else {
|
||||
|
||||
return
|
||||
}
|
||||
self.demoUnsafeTransaction(
|
||||
title: geocoded.title,
|
||||
subtitle: geocoded.subtitle,
|
||||
for: place
|
||||
)
|
||||
}
|
||||
.navigationTitle("Placemarks")
|
||||
.toolbar {
|
||||
|
||||
Button("Random") {
|
||||
|
||||
self.demoSynchronousTransaction()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var sinkCancellable: AnyCancellable?
|
||||
private let geocoder = Modern.PlacemarksDemo.Geocoder()
|
||||
@State
|
||||
private var geocoder = Modern.PlacemarksDemo.Geocoder()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_PlacemarksDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Modern.PlacemarksDemo.MainView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,8 +11,8 @@ extension Modern {
|
||||
// MARK: - Modern.PlacemarksDemo
|
||||
|
||||
/**
|
||||
Sample usages for `CoreStoreObject` transactions
|
||||
*/
|
||||
Sample usages for `CoreStoreObject` transactions
|
||||
*/
|
||||
enum PlacemarksDemo {
|
||||
|
||||
// MARK: Internal
|
||||
@@ -20,6 +20,7 @@ extension Modern {
|
||||
/**
|
||||
⭐️ Sample 1: Setting up the `DataStack` and storage
|
||||
*/
|
||||
@MainActor
|
||||
static let dataStack: DataStack = {
|
||||
|
||||
let dataStack = DataStack(
|
||||
@@ -45,7 +46,8 @@ extension Modern {
|
||||
)
|
||||
return dataStack
|
||||
}()
|
||||
|
||||
|
||||
@MainActor
|
||||
static let placePublisher: ObjectPublisher<Modern.PlacemarksDemo.Place> = {
|
||||
|
||||
let dataStack = Modern.PlacemarksDemo.dataStack
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
extension Modern.PokedexDemo {
|
||||
|
||||
|
||||
// MARK: - Modern.PokedexDemo.MainView
|
||||
|
||||
|
||||
struct MainView<ListView: View>: View {
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(
|
||||
@@ -22,16 +21,16 @@ extension Modern.PokedexDemo {
|
||||
|
||||
self.listView = listView
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
||||
self.listView()
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
.edgesIgnoringSafeArea(.vertical)
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
.ignoresSafeArea(.container, edges: .vertical)
|
||||
|
||||
if self.pokedexEntries.isEmpty {
|
||||
|
||||
@@ -56,10 +55,10 @@ extension Modern.PokedexDemo {
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Pokedex")
|
||||
.navigationTitle("Pokedex")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@ListState(
|
||||
@@ -68,28 +67,10 @@ extension Modern.PokedexDemo {
|
||||
in: Modern.PokedexDemo.dataStack
|
||||
)
|
||||
private var pokedexEntries
|
||||
|
||||
@ObservedObject
|
||||
private var service: Modern.PokedexDemo.Service = .init()
|
||||
|
||||
@State
|
||||
private var service = Modern.PokedexDemo.Service()
|
||||
|
||||
private let listView: () -> ListView
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct _Demo_Modern_PokedexDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Modern.PokedexDemo.MainView(
|
||||
listView: Modern.PokedexDemo.UIKit.ListView.init
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,64 +2,48 @@
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreStore
|
||||
import UIKit
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
extension Modern.PokedexDemo {
|
||||
|
||||
|
||||
// MARK: - Modern.PokedexDemo.Service
|
||||
|
||||
final class Service: ObservableObject {
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class Service {
|
||||
|
||||
/**
|
||||
⭐️ Sample 1: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are tuples
|
||||
*/
|
||||
private static func importPokedexEntries(
|
||||
from output: URLSession.DataTaskPublisher.Output
|
||||
) -> Future<Void, Modern.PokedexDemo.Service.Error> {
|
||||
private static func importPokedexEntries(from data: Data) async throws {
|
||||
|
||||
return .init { promise in
|
||||
do {
|
||||
|
||||
Modern.PokedexDemo.dataStack.perform(
|
||||
asynchronous: { transaction -> Void in
|
||||
|
||||
let json: Dictionary<String, Any> = try self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: output.data, options: [])
|
||||
)
|
||||
let results: [Dictionary<String, Any>] = try self.parseJSON(
|
||||
json["results"]
|
||||
)
|
||||
_ = try transaction.importUniqueObjects(
|
||||
Into<Modern.PokedexDemo.PokedexEntry>(),
|
||||
sourceArray: results.enumerated().map { (index, json) in
|
||||
(index: index, json: json)
|
||||
}
|
||||
)
|
||||
},
|
||||
success: { result in
|
||||
|
||||
promise(.success(result))
|
||||
},
|
||||
failure: { error in
|
||||
|
||||
switch error {
|
||||
|
||||
case .userError(let error as Modern.PokedexDemo.Service.Error):
|
||||
promise(.failure(error))
|
||||
|
||||
case .userError(let error):
|
||||
promise(.failure(.otherError(error)))
|
||||
|
||||
case let error:
|
||||
promise(.failure(.saveError(error)))
|
||||
try await Modern.PokedexDemo.dataStack.async.perform { transaction -> Void in
|
||||
|
||||
let json: Dictionary<String, Any> = try self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: data, options: [])
|
||||
)
|
||||
let results: [Dictionary<String, Any>] = try self.parseJSON(
|
||||
json["results"]
|
||||
)
|
||||
_ = try transaction.importUniqueObjects(
|
||||
Into<Modern.PokedexDemo.PokedexEntry>(),
|
||||
sourceArray: results.enumerated().map { index, json in
|
||||
(index: index, json: json)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
||||
throw self.mapError(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,134 +51,93 @@ extension Modern.PokedexDemo {
|
||||
⭐️ Sample 2: Importing a single JSON data into an `ImportableUniqueObject` whose `ImportSource` is a JSON `Dictionary`
|
||||
*/
|
||||
private static func importSpecies(
|
||||
for details: ObjectSnapshot<Modern.PokedexDemo.Details>,
|
||||
from output: URLSession.DataTaskPublisher.Output
|
||||
) -> Future<ObjectSnapshot<Modern.PokedexDemo.Species>, Modern.PokedexDemo.Service.Error> {
|
||||
for detailsObjectID: NSManagedObjectID,
|
||||
from data: Data
|
||||
) async throws -> ObjectSnapshot<Modern.PokedexDemo.Species> {
|
||||
|
||||
return .init { promise in
|
||||
let speciesObjectID = try await Modern.PokedexDemo.dataStack.async.perform { transaction -> NSManagedObjectID in
|
||||
|
||||
Modern.PokedexDemo.dataStack.perform(
|
||||
asynchronous: { transaction -> Modern.PokedexDemo.Species in
|
||||
|
||||
let json: Dictionary<String, Any> = try self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: output.data, options: [])
|
||||
)
|
||||
guard
|
||||
let species = try transaction.importUniqueObject(
|
||||
Into<Modern.PokedexDemo.Species>(),
|
||||
source: json
|
||||
)
|
||||
else {
|
||||
|
||||
throw Modern.PokedexDemo.Service.Error.unexpected
|
||||
}
|
||||
details.asEditable(in: transaction)?.species = species
|
||||
return species
|
||||
},
|
||||
success: { species in
|
||||
|
||||
promise(.success(species.asSnapshot(in: Modern.PokedexDemo.dataStack)!))
|
||||
},
|
||||
failure: { error in
|
||||
|
||||
switch error {
|
||||
|
||||
case .userError(let error as Modern.PokedexDemo.Service.Error):
|
||||
promise(.failure(error))
|
||||
|
||||
case .userError(let error):
|
||||
promise(.failure(.otherError(error)))
|
||||
|
||||
case let error:
|
||||
promise(.failure(.saveError(error)))
|
||||
}
|
||||
}
|
||||
let json: Dictionary<String, Any> = try self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: data, options: [])
|
||||
)
|
||||
guard
|
||||
let species = try transaction.importUniqueObject(
|
||||
Into<Modern.PokedexDemo.Species>(),
|
||||
source: json
|
||||
)
|
||||
else {
|
||||
|
||||
throw Modern.PokedexDemo.Service.Error.unexpected
|
||||
}
|
||||
transaction
|
||||
.edit(Into<Modern.PokedexDemo.Details>(), detailsObjectID)?
|
||||
.species = species
|
||||
return species.objectID()
|
||||
}
|
||||
guard
|
||||
let species: Modern.PokedexDemo.Species = Modern.PokedexDemo.dataStack.fetchExisting(speciesObjectID),
|
||||
let snapshot = species.asSnapshot()
|
||||
else {
|
||||
|
||||
throw Modern.PokedexDemo.Service.Error.unexpected
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
/**
|
||||
⭐️ Sample 3: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are JSON `Dictionary`s
|
||||
*/
|
||||
private static func importForms(
|
||||
for details: ObjectSnapshot<Modern.PokedexDemo.Details>,
|
||||
from outputs: [URLSession.DataTaskPublisher.Output]
|
||||
) -> Future<Void, Modern.PokedexDemo.Service.Error> {
|
||||
for detailsObjectID: NSManagedObjectID,
|
||||
from dataArray: [Data]
|
||||
) async throws {
|
||||
|
||||
return .init { promise in
|
||||
do {
|
||||
|
||||
Modern.PokedexDemo.dataStack.perform(
|
||||
asynchronous: { transaction -> Void in
|
||||
|
||||
let forms = try transaction.importUniqueObjects(
|
||||
Into<Modern.PokedexDemo.Form>(),
|
||||
sourceArray: outputs.map { output in
|
||||
|
||||
return try self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: output.data, options: [])
|
||||
)
|
||||
}
|
||||
)
|
||||
guard !forms.isEmpty else {
|
||||
try await Modern.PokedexDemo.dataStack.async.perform { transaction -> Void in
|
||||
|
||||
let forms = try transaction.importUniqueObjects(
|
||||
Into<Modern.PokedexDemo.Form>(),
|
||||
sourceArray: dataArray.map { data in
|
||||
|
||||
throw Modern.PokedexDemo.Service.Error.unexpected
|
||||
try self.parseJSON(
|
||||
try JSONSerialization.jsonObject(with: data, options: [])
|
||||
) as [String: Any]
|
||||
}
|
||||
details.asEditable(in: transaction)?.forms = forms
|
||||
},
|
||||
success: {
|
||||
)
|
||||
guard !forms.isEmpty else {
|
||||
|
||||
promise(.success(()))
|
||||
},
|
||||
failure: { error in
|
||||
|
||||
switch error {
|
||||
|
||||
case .userError(let error as Modern.PokedexDemo.Service.Error):
|
||||
promise(.failure(error))
|
||||
|
||||
case .userError(let error):
|
||||
promise(.failure(.otherError(error)))
|
||||
|
||||
case let error:
|
||||
promise(.failure(.saveError(error)))
|
||||
}
|
||||
throw Modern.PokedexDemo.Service.Error.unexpected
|
||||
}
|
||||
)
|
||||
transaction
|
||||
.edit(Into<Modern.PokedexDemo.Details>(), detailsObjectID)?
|
||||
.forms = forms
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
||||
throw self.mapError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
private(set) var isLoading: Bool = false {
|
||||
|
||||
willSet {
|
||||
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var lastError: (error: Modern.PokedexDemo.Service.Error, retry: () -> Void)? {
|
||||
|
||||
willSet {
|
||||
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var isLoading: Bool = false
|
||||
|
||||
init() {}
|
||||
|
||||
static func parseJSON<Output>(
|
||||
|
||||
static nonisolated func parseJSON<Output>(
|
||||
_ json: Any?,
|
||||
file: StaticString = #file,
|
||||
line: Int = #line
|
||||
) throws -> Output {
|
||||
|
||||
|
||||
switch json {
|
||||
|
||||
|
||||
case let json as Output:
|
||||
return json
|
||||
|
||||
|
||||
case let any:
|
||||
throw Modern.PokedexDemo.Service.Error.parseError(
|
||||
expected: Output.self,
|
||||
@@ -203,20 +146,20 @@ extension Modern.PokedexDemo {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static func parseJSON<JSONType, Output>(
|
||||
|
||||
static nonisolated func parseJSON<JSONType, Output>(
|
||||
_ json: Any?,
|
||||
transformer: (JSONType) throws -> Output?,
|
||||
file: StaticString = #file,
|
||||
line: Int = #line
|
||||
) throws -> Output {
|
||||
|
||||
|
||||
switch json {
|
||||
|
||||
|
||||
case let json as JSONType:
|
||||
let transformed = try transformer(json)
|
||||
if let json = transformed {
|
||||
|
||||
|
||||
return json
|
||||
}
|
||||
throw Modern.PokedexDemo.Service.Error.parseError(
|
||||
@@ -224,7 +167,7 @@ extension Modern.PokedexDemo {
|
||||
actual: type(of: transformed),
|
||||
file: "\(file):\(line)"
|
||||
)
|
||||
|
||||
|
||||
case let any:
|
||||
throw Modern.PokedexDemo.Service.Error.parseError(
|
||||
expected: Output.self,
|
||||
@@ -233,153 +176,235 @@ extension Modern.PokedexDemo {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func fetchPokedexEntries() {
|
||||
|
||||
self.cancellable["pokedexEntries"] = self.pokedexEntries
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents(
|
||||
receiveSubscription: { [weak self] _ in
|
||||
|
||||
guard let self = self else {
|
||||
|
||||
return
|
||||
}
|
||||
self.lastError = nil
|
||||
self.isLoading = true
|
||||
}
|
||||
)
|
||||
.sink(
|
||||
receiveCompletion: { [weak self] completion in
|
||||
|
||||
guard let self = self else {
|
||||
|
||||
return
|
||||
}
|
||||
self.isLoading = false
|
||||
switch completion {
|
||||
|
||||
case .finished:
|
||||
self.lastError = nil
|
||||
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
self.lastError = (
|
||||
error: error,
|
||||
retry: { [weak self] in
|
||||
|
||||
self?.fetchPokedexEntries()
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
receiveValue: {}
|
||||
)
|
||||
|
||||
self.pokedexEntriesTask?.cancel()
|
||||
self.pokedexEntriesTask = Task { [weak self] in
|
||||
|
||||
await self?.runFetchPokedexEntries()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchDetails(for pokedexEntry: ObjectSnapshot<Modern.PokedexDemo.PokedexEntry>) {
|
||||
|
||||
self.fetchSpeciesIfNeeded(for: pokedexEntry)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var cancellable: Dictionary<String, AnyCancellable> = [:]
|
||||
|
||||
private lazy var pokedexEntries: AnyPublisher<Void, Modern.PokedexDemo.Service.Error> = URLSession.shared
|
||||
.dataTaskPublisher(
|
||||
for: URL(string: "https://pokeapi.co/api/v2/pokemon?limit=10000&offset=0")!
|
||||
)
|
||||
.mapError({ .networkError($0) })
|
||||
.flatMap(Self.importPokedexEntries(from:))
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
private func fetchSpeciesIfNeeded(for pokedexEntry: ObjectSnapshot<Modern.PokedexDemo.PokedexEntry>) {
|
||||
|
||||
guard let details = pokedexEntry.$details?.snapshot else {
|
||||
|
||||
return
|
||||
}
|
||||
if let species = details.$species?.snapshot {
|
||||
|
||||
self.fetchFormsIfNeeded(for: species)
|
||||
self.fetchFormsIfNeeded(
|
||||
key: species.$id,
|
||||
detailsObjectID: details.objectID(),
|
||||
species: species
|
||||
)
|
||||
return
|
||||
}
|
||||
self.cancellable["species.\(pokedexEntry.$id)"] = URLSession.shared
|
||||
.dataTaskPublisher(for: pokedexEntry.$speciesURL)
|
||||
.mapError({ .networkError($0) })
|
||||
.flatMap({ Self.importSpecies(for: details, from: $0) })
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
|
||||
switch completion {
|
||||
|
||||
case .finished:
|
||||
break
|
||||
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
}
|
||||
},
|
||||
receiveValue: { species in
|
||||
|
||||
self.fetchFormsIfNeeded(for: species)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func fetchFormsIfNeeded(for species: ObjectSnapshot<Modern.PokedexDemo.Species>) {
|
||||
|
||||
guard
|
||||
let details = species.$details?.snapshot,
|
||||
details.$forms.isEmpty
|
||||
else {
|
||||
let key = pokedexEntry.$id
|
||||
guard self.detailTasks[key] == nil else {
|
||||
|
||||
return
|
||||
}
|
||||
self.cancellable["forms.\(species.$id)"] = species
|
||||
.$formsURLs
|
||||
.map(
|
||||
{
|
||||
URLSession.shared
|
||||
.dataTaskPublisher(for: $0)
|
||||
.mapError({ Modern.PokedexDemo.Service.Error.networkError($0) })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
)
|
||||
.reduce(
|
||||
into: Just<[URLSession.DataTaskPublisher.Output]>([])
|
||||
.setFailureType(to: Modern.PokedexDemo.Service.Error.self)
|
||||
.eraseToAnyPublisher(),
|
||||
{ (result, publisher) in
|
||||
result = result
|
||||
.zip(publisher, { $0 + [$1] })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
)
|
||||
.flatMap({ Self.importForms(for: details, from: $0) })
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
|
||||
switch completion {
|
||||
|
||||
case .finished:
|
||||
break
|
||||
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
}
|
||||
},
|
||||
receiveValue: { _ in }
|
||||
let speciesURL = pokedexEntry.$speciesURL
|
||||
let detailsObjectID = details.objectID()
|
||||
self.detailTasks[key] = Task { [weak self] in
|
||||
|
||||
guard let self else {
|
||||
|
||||
return
|
||||
}
|
||||
defer {
|
||||
|
||||
self.detailTasks.removeValue(forKey: key)
|
||||
}
|
||||
await self.fetchSpecies(
|
||||
key: key,
|
||||
detailsObjectID: detailsObjectID,
|
||||
speciesURL: speciesURL
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@ObservationIgnored
|
||||
private static let pokedexURL = URL(
|
||||
string: "https://pokeapi.co/api/v2/pokemon?limit=10000&offset=0"
|
||||
)!
|
||||
|
||||
@ObservationIgnored
|
||||
private var pokedexEntriesTask: Task<Void, Never>?
|
||||
|
||||
@ObservationIgnored
|
||||
private var detailTasks: [String: Task<Void, Never>] = [:]
|
||||
|
||||
private static func mapError(_ error: CoreStoreError) -> Modern.PokedexDemo.Service.Error {
|
||||
|
||||
switch error {
|
||||
case .userError(let error as Modern.PokedexDemo.Service.Error):
|
||||
return error
|
||||
|
||||
case .userError(let error):
|
||||
return .otherError(error)
|
||||
|
||||
case let error:
|
||||
return .saveError(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func runFetchPokedexEntries() async {
|
||||
|
||||
self.isLoading = true
|
||||
defer {
|
||||
|
||||
self.isLoading = false
|
||||
self.pokedexEntriesTask = nil
|
||||
}
|
||||
|
||||
do {
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(from: Self.pokedexURL)
|
||||
try Task.checkCancellation()
|
||||
try await Self.importPokedexEntries(from: data)
|
||||
}
|
||||
catch is CancellationError {
|
||||
|
||||
return
|
||||
}
|
||||
catch let error as Modern.PokedexDemo.Service.Error {
|
||||
|
||||
print(error)
|
||||
}
|
||||
catch let error as URLError {
|
||||
|
||||
print(Modern.PokedexDemo.Service.Error.networkError(error))
|
||||
}
|
||||
catch {
|
||||
|
||||
print(Modern.PokedexDemo.Service.Error.otherError(error))
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchSpecies(
|
||||
key: String,
|
||||
detailsObjectID: NSManagedObjectID,
|
||||
speciesURL: URL
|
||||
) async {
|
||||
|
||||
do {
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(from: speciesURL)
|
||||
try Task.checkCancellation()
|
||||
|
||||
let species = try await Self.importSpecies(
|
||||
for: detailsObjectID,
|
||||
from: data
|
||||
)
|
||||
guard species.$details?.snapshot?.$forms.isEmpty == true else {
|
||||
|
||||
return
|
||||
}
|
||||
await self.fetchForms(
|
||||
detailsObjectID: detailsObjectID,
|
||||
formsURLs: species.$formsURLs
|
||||
)
|
||||
}
|
||||
catch is CancellationError {
|
||||
|
||||
return
|
||||
}
|
||||
catch let error as Modern.PokedexDemo.Service.Error {
|
||||
|
||||
print(error)
|
||||
}
|
||||
catch let error as URLError {
|
||||
|
||||
print(Modern.PokedexDemo.Service.Error.networkError(error))
|
||||
}
|
||||
catch {
|
||||
|
||||
print(Modern.PokedexDemo.Service.Error.otherError(error))
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchFormsIfNeeded(
|
||||
key: String,
|
||||
detailsObjectID: NSManagedObjectID,
|
||||
species: ObjectSnapshot<Modern.PokedexDemo.Species>
|
||||
) {
|
||||
|
||||
guard species.$details?.snapshot?.$forms.isEmpty == true else {
|
||||
|
||||
return
|
||||
}
|
||||
guard self.detailTasks[key] == nil else {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let formsURLs = species.$formsURLs
|
||||
self.detailTasks[key] = Task { [weak self] in
|
||||
|
||||
guard let self else {
|
||||
|
||||
return
|
||||
}
|
||||
defer {
|
||||
|
||||
self.detailTasks.removeValue(forKey: key)
|
||||
}
|
||||
await self.fetchForms(
|
||||
detailsObjectID: detailsObjectID,
|
||||
formsURLs: formsURLs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchForms(
|
||||
detailsObjectID: NSManagedObjectID,
|
||||
formsURLs: [URL]
|
||||
) async {
|
||||
|
||||
do {
|
||||
|
||||
var dataArray: [Data] = []
|
||||
dataArray.reserveCapacity(formsURLs.count)
|
||||
|
||||
for url in formsURLs {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
try Task.checkCancellation()
|
||||
dataArray.append(data)
|
||||
}
|
||||
try await Self.importForms(
|
||||
for: detailsObjectID,
|
||||
from: dataArray
|
||||
)
|
||||
}
|
||||
catch is CancellationError {
|
||||
|
||||
return
|
||||
}
|
||||
catch let error as Modern.PokedexDemo.Service.Error {
|
||||
|
||||
print(error)
|
||||
}
|
||||
catch let error as URLError {
|
||||
|
||||
print(Modern.PokedexDemo.Service.Error.networkError(error))
|
||||
}
|
||||
catch {
|
||||
|
||||
print(Modern.PokedexDemo.Service.Error.otherError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Modern.PokedexDemo.Service.Error
|
||||
|
||||
|
||||
enum Error: Swift.Error {
|
||||
|
||||
|
||||
case networkError(URLError)
|
||||
case parseError(expected: Any.Type, actual: Any.Type, file: String)
|
||||
case saveError(CoreStoreError)
|
||||
|
||||
@@ -12,12 +12,11 @@ extension Modern.PokedexDemo.UIKit {
|
||||
// MARK: - Modern.PokedexDemo.ListView
|
||||
|
||||
struct ListView: UIViewControllerRepresentable {
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
|
||||
init() {
|
||||
|
||||
self.service = Modern.PokedexDemo.Service.init()
|
||||
self._service = State(initialValue: Modern.PokedexDemo.Service())
|
||||
self.listPublisher = Modern.PokedexDemo.dataStack
|
||||
.publishList(
|
||||
From<Modern.PokedexDemo.PokedexEntry>()
|
||||
@@ -41,30 +40,20 @@ extension Modern.PokedexDemo.UIKit {
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {}
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@ObservedObject
|
||||
|
||||
@State
|
||||
private var service: Modern.PokedexDemo.Service
|
||||
|
||||
|
||||
private let listPublisher: ListPublisher<Modern.PokedexDemo.PokedexEntry>
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_PokedexDemo_UIKit_ListView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let service = Modern.PokedexDemo.Service()
|
||||
service.fetchPokedexEntries()
|
||||
|
||||
return Modern.PokedexDemo.UIKit.ListView()
|
||||
}
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
Modern.PokedexDemo.UIKit.ListView()
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,12 +11,13 @@ extension Modern {
|
||||
// MARK: - Modern.PokedexDemo
|
||||
|
||||
/**
|
||||
Sample usages for importing external data into `CoreStoreObject` attributes
|
||||
*/
|
||||
Sample usages for importing external data into `CoreStoreObject` attributes
|
||||
*/
|
||||
enum PokedexDemo {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
@MainActor
|
||||
static let dataStack: DataStack = {
|
||||
|
||||
let dataStack = DataStack(
|
||||
@@ -49,6 +50,7 @@ extension Modern {
|
||||
return dataStack
|
||||
}()
|
||||
|
||||
@MainActor
|
||||
static let pokedexEntries: ListPublisher<Modern.PokedexDemo.PokedexEntry> = Modern.PokedexDemo.dataStack.publishList(
|
||||
From<Modern.PokedexDemo.PokedexEntry>()
|
||||
.orderBy(.ascending(\.$index))
|
||||
|
||||
@@ -15,18 +15,22 @@ extension Modern.TimeZonesDemo {
|
||||
// MARK: Internal
|
||||
|
||||
init(title: String, subtitle: String) {
|
||||
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Text(self.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(self.subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
@@ -40,19 +44,3 @@ extension Modern.TimeZonesDemo {
|
||||
fileprivate let subtitle: String
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_TimeZonesDemo_ItemView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
Modern.TimeZonesDemo.ItemView(
|
||||
title: "Item Title",
|
||||
subtitle: "A subtitle caption for this item"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
extension Modern.TimeZonesDemo {
|
||||
|
||||
// MARK: - Modern.TimeZonesDemo.ListView
|
||||
|
||||
|
||||
struct ListView: View {
|
||||
|
||||
// MARK: Internal
|
||||
@@ -53,15 +53,18 @@ extension Modern.TimeZonesDemo {
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
|
||||
List {
|
||||
|
||||
ForEach(self.values, id: \.title) { item in
|
||||
|
||||
Modern.TimeZonesDemo.ItemView(
|
||||
title: item.title,
|
||||
subtitle: item.subtitle
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(self.title)
|
||||
.navigationTitle(self.title)
|
||||
}
|
||||
|
||||
|
||||
@@ -71,24 +74,3 @@ extension Modern.TimeZonesDemo {
|
||||
private let values: [(title: String, subtitle: String)]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_TimeZonesDemo_ListView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Modern.TimeZonesDemo.ListView(
|
||||
title: "Title",
|
||||
objects: try! Modern.TimeZonesDemo.dataStack.fetchAll(
|
||||
From<Modern.TimeZonesDemo.TimeZone>()
|
||||
.orderBy(.ascending(\.$name))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
extension Modern.TimeZonesDemo {
|
||||
|
||||
// MARK: - Modern.TimeZonesDemo.MainView
|
||||
|
||||
|
||||
struct MainView: View {
|
||||
|
||||
/**
|
||||
@@ -61,11 +61,11 @@ extension Modern.TimeZonesDemo {
|
||||
return try! Modern.TimeZonesDemo.dataStack.fetchAll(
|
||||
From<Modern.TimeZonesDemo.TimeZone>()
|
||||
.where((-secondsIn3Hours ... secondsIn3Hours) ~= \.$secondsFromGMT)
|
||||
/// equivalent to:
|
||||
/// ```
|
||||
/// .where(\.$secondsFromGMT >= -secondsIn3Hours
|
||||
/// && \.$secondsFromGMT <= secondsIn3Hours)
|
||||
/// ```
|
||||
/// equivalent to:
|
||||
/// ```
|
||||
/// .where(\.$secondsFromGMT >= -secondsIn3Hours
|
||||
/// && \.$secondsFromGMT <= secondsIn3Hours)
|
||||
/// ```
|
||||
.orderBy(.ascending(\.$secondsFromGMT))
|
||||
)
|
||||
}
|
||||
@@ -137,36 +137,54 @@ extension Modern.TimeZonesDemo {
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
|
||||
List {
|
||||
Section(header: Text("Fetching objects")) {
|
||||
|
||||
Section("Fetching objects") {
|
||||
|
||||
ForEach(self.fetchingItems, id: \.title) { item in
|
||||
Menu.ItemView(
|
||||
title: item.title,
|
||||
|
||||
NavigationLink(
|
||||
destination: {
|
||||
|
||||
Modern.TimeZonesDemo.ListView(
|
||||
title: item.title,
|
||||
objects: item.objects()
|
||||
)
|
||||
},
|
||||
label: {
|
||||
|
||||
Menu.ItemView(
|
||||
title: item.title
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Section(header: Text("Querying raw values")) {
|
||||
Section("Querying raw values") {
|
||||
|
||||
ForEach(self.queryingItems, id: \.title) { item in
|
||||
Menu.ItemView(
|
||||
title: item.title,
|
||||
|
||||
NavigationLink(
|
||||
destination: {
|
||||
|
||||
Modern.TimeZonesDemo.ListView(
|
||||
title: item.title,
|
||||
value: item.value()
|
||||
)
|
||||
},
|
||||
label: {
|
||||
|
||||
Menu.ItemView(
|
||||
title: item.title
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
.navigationBarTitle("Time Zones")
|
||||
.listStyle(.grouped)
|
||||
.navigationTitle("Time Zones")
|
||||
}
|
||||
|
||||
|
||||
@@ -217,18 +235,3 @@ extension Modern.TimeZonesDemo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_TimeZonesDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Modern.TimeZonesDemo.MainView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,8 +11,8 @@ extension Modern {
|
||||
// MARK: - Modern.TimeZonesDemo
|
||||
|
||||
/**
|
||||
Sample usages for creating Fetch and Query clauses for `CoreStoreObject`s
|
||||
*/
|
||||
Sample usages for creating Fetch and Query clauses for `CoreStoreObject`s
|
||||
*/
|
||||
enum TimeZonesDemo {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
Reference in New Issue
Block a user