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