mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-04-19 23:41:20 +02:00
WIP: new demo app
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Contacts
|
||||
import CoreLocation
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Modern.PlacemarksDemo
|
||||
|
||||
extension Modern.PlacemarksDemo {
|
||||
|
||||
// MARK: Geocoder
|
||||
|
||||
final class Geocoder {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
func geocode(
|
||||
place: ObjectSnapshot<Modern.PlacemarksDemo.Place>,
|
||||
completion: @escaping (_ title: String?, _ subtitle: String?) -> Void
|
||||
) {
|
||||
|
||||
self.geocoder?.cancelGeocode()
|
||||
|
||||
let geocoder = CLGeocoder()
|
||||
self.geocoder = geocoder
|
||||
geocoder.reverseGeocodeLocation(
|
||||
CLLocation(latitude: place.$latitude, longitude: place.$longitude),
|
||||
completionHandler: { (placemarks, error) -> Void in
|
||||
|
||||
defer {
|
||||
|
||||
self.geocoder = nil
|
||||
}
|
||||
guard let placemark = placemarks?.first else {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let address = CNMutablePostalAddress()
|
||||
address.street = placemark.thoroughfare ?? ""
|
||||
address.subLocality = placemark.subThoroughfare ?? ""
|
||||
address.city = placemark.locality ?? ""
|
||||
address.subAdministrativeArea = placemark.subAdministrativeArea ?? ""
|
||||
address.state = placemark.administrativeArea ?? ""
|
||||
address.postalCode = placemark.postalCode ?? ""
|
||||
address.country = placemark.country ?? ""
|
||||
address.isoCountryCode = placemark.isoCountryCode ?? ""
|
||||
|
||||
completion(
|
||||
placemark.name,
|
||||
CNPostalAddressFormatter.string(
|
||||
from: address,
|
||||
style: .mailingAddress
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var geocoder: CLGeocoder?
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreLocation
|
||||
import Combine
|
||||
import CoreStore
|
||||
import Foundation
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Modern.PlacemarksDemo
|
||||
|
||||
extension Modern.PlacemarksDemo {
|
||||
|
||||
// MARK: - Modern.PlacemarksDemo.MainView
|
||||
|
||||
struct MainView: View {
|
||||
|
||||
/**
|
||||
⭐️ Sample 1: Asynchronous transactions
|
||||
*/
|
||||
private func demoAsynchronousTransaction(coordinate: CLLocationCoordinate2D) {
|
||||
|
||||
Modern.PlacemarksDemo.dataStack.perform(
|
||||
asynchronous: { (transaction) in
|
||||
|
||||
let place = self.place.asEditable(in: transaction)
|
||||
place?.annotation = .init(coordinate: coordinate)
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
⭐️ Sample 2: Synchronous transactions
|
||||
|
||||
- Important: `perform(synchronous:)` was used here for illustration purposes. In practice, `perform(asynchronous:completion:)` is the preferred transaction type as synchronous transactions are very likely to cause deadlocks.
|
||||
*/
|
||||
private func demoSynchronousTransaction() {
|
||||
|
||||
_ = try? Modern.PlacemarksDemo.dataStack.perform(
|
||||
synchronous: { (transaction) in
|
||||
|
||||
let place = self.place.asEditable(in: transaction)
|
||||
place?.setRandomLocation()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
⭐️ Sample 3: Unsafe transactions
|
||||
|
||||
- Important: `beginUnsafe()` was used here for illustration purposes. In practice, `perform(asynchronous:completion:)` is the preferred transaction type. Use Unsafe Transactions only when you need to bypass CoreStore's serialized transactions.
|
||||
*/
|
||||
private func demoUnsafeTransaction(
|
||||
title: String?,
|
||||
subtitle: String?,
|
||||
for snapshot: ObjectSnapshot<Modern.PlacemarksDemo.Place>
|
||||
) {
|
||||
let transaction = Modern.PlacemarksDemo.dataStack.beginUnsafe()
|
||||
let place = snapshot.asEditable(in: transaction)
|
||||
place?.title = title
|
||||
place?.subtitle = subtitle
|
||||
|
||||
transaction.commit { (error) in
|
||||
|
||||
print("Commit failed: \(error as Any)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
@ObservedObject
|
||||
var place: ObjectPublisher<Modern.PlacemarksDemo.Place>
|
||||
|
||||
init() {
|
||||
|
||||
self.place = Modern.PlacemarksDemo.placePublisher
|
||||
self.sinkCancellable = self.place.sink(
|
||||
receiveCompletion: { _ in
|
||||
|
||||
// Deleted, do nothing
|
||||
},
|
||||
receiveValue: { [self] (snapshot) in
|
||||
|
||||
self.geocoder.geocode(place: snapshot) { (title, subtitle) in
|
||||
|
||||
guard self.place.snapshot == snapshot else {
|
||||
|
||||
return
|
||||
}
|
||||
self.demoUnsafeTransaction(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
for: snapshot
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
Modern.PlacemarksDemo.MapView(
|
||||
place: self.place.snapshot,
|
||||
onTap: { coordinate in
|
||||
|
||||
self.demoAsynchronousTransaction(coordinate: coordinate)
|
||||
}
|
||||
)
|
||||
.overlay(
|
||||
InstructionsView(
|
||||
("Random", "Sets random coordinate"),
|
||||
("Tap", "Sets to tapped coordinate")
|
||||
)
|
||||
.padding(.leading, 10)
|
||||
.padding(.bottom, 40),
|
||||
alignment: .bottomLeading
|
||||
)
|
||||
.navigationBarTitle("Placemarks")
|
||||
.navigationBarItems(
|
||||
trailing: Button("Random") {
|
||||
|
||||
self.demoSynchronousTransaction()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var sinkCancellable: AnyCancellable? = nil
|
||||
private let geocoder = Modern.PlacemarksDemo.Geocoder()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Modern_PlacemarksDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Modern.PlacemarksDemo.MainView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreLocation
|
||||
import CoreStore
|
||||
import MapKit
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Modern.PlacemarksDemo
|
||||
|
||||
extension Modern.PlacemarksDemo {
|
||||
|
||||
// MARK: - Modern.PlacemarksDemo.MapView
|
||||
|
||||
struct MapView: UIViewRepresentable {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
var place: ObjectSnapshot<Modern.PlacemarksDemo.Place>?
|
||||
|
||||
let onTap: (CLLocationCoordinate2D) -> Void
|
||||
|
||||
// MARK: UIViewRepresentable
|
||||
|
||||
typealias UIViewType = MKMapView
|
||||
|
||||
func makeUIView(context: Context) -> UIViewType {
|
||||
|
||||
let coordinator = context.coordinator
|
||||
|
||||
let mapView = MKMapView()
|
||||
mapView.delegate = coordinator
|
||||
mapView.addGestureRecognizer(
|
||||
UITapGestureRecognizer(
|
||||
target: coordinator,
|
||||
action: #selector(coordinator.tapGestureRecognized(_:))
|
||||
)
|
||||
)
|
||||
return mapView
|
||||
}
|
||||
|
||||
func updateUIView(_ view: UIViewType, context: Context) {
|
||||
|
||||
let currentAnnotations = view.annotations
|
||||
view.removeAnnotations(currentAnnotations)
|
||||
|
||||
guard let newAnnotation = self.place?.$annotation else {
|
||||
|
||||
return
|
||||
}
|
||||
view.addAnnotation(newAnnotation)
|
||||
view.setCenter(newAnnotation.coordinate, animated: true)
|
||||
view.selectAnnotation(newAnnotation, animated: true)
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject, MKMapViewDelegate {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(_ parent: MapView) {
|
||||
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
// MARK: MKMapViewDelegate
|
||||
|
||||
@objc dynamic func mapView(
|
||||
_ mapView: MKMapView,
|
||||
viewFor annotation: MKAnnotation
|
||||
) -> MKAnnotationView? {
|
||||
|
||||
let identifier = "MKAnnotationView"
|
||||
var annotationView: MKPinAnnotationView! = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView
|
||||
if annotationView == nil {
|
||||
|
||||
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
|
||||
annotationView.isEnabled = true
|
||||
annotationView.canShowCallout = true
|
||||
annotationView.animatesDrop = true
|
||||
}
|
||||
else {
|
||||
|
||||
annotationView.annotation = annotation
|
||||
}
|
||||
return annotationView
|
||||
}
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
@objc
|
||||
fileprivate dynamic func tapGestureRecognized(_ gesture: UILongPressGestureRecognizer) {
|
||||
|
||||
guard
|
||||
case let mapView as MKMapView = gesture.view,
|
||||
gesture.state == .recognized
|
||||
else {
|
||||
|
||||
return
|
||||
}
|
||||
let coordinate = mapView.convert(
|
||||
gesture.location(in: mapView),
|
||||
toCoordinateFrom: mapView
|
||||
)
|
||||
self.parent.onTap(coordinate)
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var parent: MapView
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
import struct CoreLocation.CLLocationCoordinate2D
|
||||
import protocol MapKit.MKAnnotation
|
||||
|
||||
// MARK: - Modern.PlacemarksDemo
|
||||
|
||||
extension Modern.PlacemarksDemo {
|
||||
|
||||
// MARK: - Modern.PlacemarksDemo.Place
|
||||
|
||||
final class Place: CoreStoreObject {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
@Field.Stored("latitude")
|
||||
var latitude: Double = 0
|
||||
|
||||
@Field.Stored("longitude")
|
||||
var longitude: Double = 0
|
||||
|
||||
@Field.Stored("title")
|
||||
var title: String?
|
||||
|
||||
@Field.Stored("subtitle")
|
||||
var subtitle: String?
|
||||
|
||||
@Field.Virtual(
|
||||
"annotation",
|
||||
customGetter: { object, field in
|
||||
|
||||
Annotation(
|
||||
latitude: object.$latitude.value,
|
||||
longitude: object.$longitude.value,
|
||||
title: object.$title.value,
|
||||
subtitle: object.$subtitle.value
|
||||
)
|
||||
},
|
||||
customSetter: { object, field, newValue in
|
||||
|
||||
object.$latitude.value = newValue.coordinate.latitude
|
||||
object.$longitude.value = newValue.coordinate.longitude
|
||||
object.$title.value = "\(newValue.coordinate.latitude), \(newValue.coordinate.longitude)"
|
||||
object.$subtitle.value = nil
|
||||
}
|
||||
)
|
||||
var annotation: Annotation
|
||||
|
||||
func setRandomLocation() {
|
||||
|
||||
self.latitude = Double(arc4random_uniform(180)) - 90
|
||||
self.longitude = Double(arc4random_uniform(360)) - 180
|
||||
self.title = "\(self.latitude), \(self.longitude)"
|
||||
self.subtitle = nil
|
||||
}
|
||||
|
||||
// MARK: - Annotation
|
||||
|
||||
final class Annotation: NSObject, MKAnnotation {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(coordinate: CLLocationCoordinate2D) {
|
||||
|
||||
self.coordinate = coordinate
|
||||
self.title = nil
|
||||
self.subtitle = nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: MKAnnotation
|
||||
|
||||
let coordinate: CLLocationCoordinate2D
|
||||
let title: String?
|
||||
let subtitle: String?
|
||||
|
||||
|
||||
// MARK: NSObjectProtocol
|
||||
|
||||
override func isEqual(_ object: Any?) -> Bool {
|
||||
|
||||
guard case let object as Annotation = object else {
|
||||
|
||||
return false
|
||||
}
|
||||
return self.coordinate.latitude == object.coordinate.latitude
|
||||
&& self.coordinate.longitude == object.coordinate.longitude
|
||||
&& self.title == object.title
|
||||
&& self.subtitle == object.subtitle
|
||||
}
|
||||
|
||||
override var hash: Int {
|
||||
|
||||
var hasher = Hasher()
|
||||
hasher.combine(self.coordinate.latitude)
|
||||
hasher.combine(self.coordinate.longitude)
|
||||
hasher.combine(self.title)
|
||||
hasher.combine(self.subtitle)
|
||||
return hasher.finalize()
|
||||
}
|
||||
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
fileprivate init(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
title: String?,
|
||||
subtitle: String?
|
||||
) {
|
||||
self.coordinate = .init(latitude: latitude, longitude: longitude)
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
// MARK: - Modern
|
||||
|
||||
extension Modern {
|
||||
|
||||
// MARK: - Modern.PlacemarksDemo
|
||||
|
||||
/**
|
||||
Sample usages for `CoreStoreObject` transactions
|
||||
*/
|
||||
enum PlacemarksDemo {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static let dataStack: DataStack = {
|
||||
|
||||
let dataStack = DataStack(
|
||||
CoreStoreSchema(
|
||||
modelVersion: "V1",
|
||||
entities: [
|
||||
Entity<Modern.PlacemarksDemo.Place>("Place")
|
||||
],
|
||||
versionLock: [
|
||||
"Place": [0xa7eec849af5e8fcb, 0x638e69c040090319, 0x4e976d66ed400447, 0x18e96bc0438d07bb]
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
- Important: `addStorageAndWait(_:)` and `perform(synchronous:)` methods were used here to simplify initializing the demo, but in practice the asynchronous function variants are recommended.
|
||||
*/
|
||||
try! dataStack.addStorageAndWait(
|
||||
SQLiteStore(
|
||||
fileName: "Modern.PlacemarksDemo.sqlite",
|
||||
localStorageOptions: .recreateStoreOnModelMismatch
|
||||
)
|
||||
)
|
||||
return dataStack
|
||||
}()
|
||||
|
||||
static let placePublisher: ObjectPublisher<Modern.PlacemarksDemo.Place> = {
|
||||
|
||||
let dataStack = Modern.PlacemarksDemo.dataStack
|
||||
if let place = try! dataStack.fetchOne(From<Place>()) {
|
||||
|
||||
return dataStack.publishObject(place)
|
||||
}
|
||||
_ = try! dataStack.perform(
|
||||
synchronous: { (transaction) in
|
||||
|
||||
let place = transaction.create(Into<Place>())
|
||||
place.setRandomLocation()
|
||||
}
|
||||
)
|
||||
let place = try! dataStack.fetchOne(From<Place>())
|
||||
return dataStack.publishObject(place!)
|
||||
}()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user