mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-09-10 03:41:53 +02:00
WIP
This commit is contained in:
@@ -4,66 +4,75 @@
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
// MARK: - ImageDownloader
|
||||
|
||||
final class ImageDownloader: ObservableObject {
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
@MainActor
|
||||
final class ImageDownloader {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
private(set) var image: UIImage?
|
||||
|
||||
|
||||
let url: URL?
|
||||
|
||||
|
||||
init(url: URL?) {
|
||||
|
||||
|
||||
self.url = url
|
||||
guard let url = url else {
|
||||
|
||||
guard let url else {
|
||||
|
||||
return
|
||||
}
|
||||
if let image = Self.cache[url] {
|
||||
|
||||
|
||||
self.image = image
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func fetchImage(completion: @escaping (UIImage) -> Void = { _ in }) {
|
||||
|
||||
guard let url = url else {
|
||||
|
||||
|
||||
guard let url else {
|
||||
|
||||
return
|
||||
}
|
||||
if let image = Self.cache[url] {
|
||||
|
||||
self.objectWillChange.send()
|
||||
|
||||
self.image = image
|
||||
completion(image)
|
||||
return
|
||||
}
|
||||
self.cancellable = URLSession.shared
|
||||
.dataTaskPublisher(for: url)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { output in
|
||||
|
||||
if let image = UIImage(data: output.data) {
|
||||
|
||||
Self.cache[url] = image
|
||||
self.objectWillChange.send()
|
||||
self.image = image
|
||||
completion(image)
|
||||
}
|
||||
|
||||
self.task = Task { [weak self] in
|
||||
|
||||
guard let self else {
|
||||
|
||||
return
|
||||
}
|
||||
do {
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
guard
|
||||
!Task.isCancelled,
|
||||
let image = UIImage(data: data)
|
||||
else {
|
||||
|
||||
return
|
||||
}
|
||||
)
|
||||
Self.cache[url] = image
|
||||
self.image = image
|
||||
completion(image)
|
||||
}
|
||||
catch {
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
|
||||
private static var cache: [URL: UIImage] = [:]
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
|
||||
private var task: Task<Void, Never>?
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - LazyView
|
||||
|
||||
struct LazyView<Content: View>: View {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(_ load: @escaping () -> Content) {
|
||||
|
||||
self.load = load
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: Content {
|
||||
|
||||
self.load()
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let load: () -> Content
|
||||
}
|
||||
@@ -10,34 +10,36 @@ extension Menu {
|
||||
|
||||
// MARK: - Menu.ItemView
|
||||
|
||||
struct ItemView<Destination: View>: View {
|
||||
struct ItemView: View {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(
|
||||
title: String,
|
||||
subtitle: String? = nil,
|
||||
destination: @escaping () -> Destination
|
||||
isEnabled: Bool = true
|
||||
) {
|
||||
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.destination = destination
|
||||
self.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: LazyView(self.destination)) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(self.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
self.subtitle.map {
|
||||
Text($0)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Text(self.title)
|
||||
.font(.headline)
|
||||
.foregroundStyle(self.isEnabled ? .primary : .secondary)
|
||||
|
||||
self.subtitle.map {
|
||||
|
||||
Text($0)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,25 +49,6 @@ extension Menu {
|
||||
|
||||
fileprivate let title: String
|
||||
fileprivate let subtitle: String?
|
||||
fileprivate let destination: () -> Destination
|
||||
fileprivate let isEnabled: Bool
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Menu_ItemView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
Menu.ItemView(
|
||||
title: "Item Title",
|
||||
subtitle: "A subtitle caption for this item",
|
||||
destination: {
|
||||
Color.blue
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,124 +11,54 @@ import SwiftUI
|
||||
extension Menu {
|
||||
|
||||
// MARK: - Menu.MainView
|
||||
|
||||
|
||||
struct MainView: View {
|
||||
|
||||
@State
|
||||
private var selection: Menu.Route?
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
Section(header: Text("Modern (CoreStoreObject subclasses)")) {
|
||||
Menu.ItemView(
|
||||
title: "Placemarks",
|
||||
subtitle: "Making changes using Transactions",
|
||||
destination: {
|
||||
Modern.PlacemarksDemo.MainView()
|
||||
|
||||
NavigationSplitView(
|
||||
sidebar: {
|
||||
|
||||
List(selection: self.$selection) {
|
||||
|
||||
ForEach(Menu.Section.allCases, id: \.self) { section in
|
||||
|
||||
Section(section.rawValue) {
|
||||
|
||||
ForEach(section.routes) { route in
|
||||
|
||||
Menu.ItemView(
|
||||
title: route.title,
|
||||
subtitle: route.subtitle,
|
||||
isEnabled: route.isEnabled
|
||||
)
|
||||
.tag(route as Menu.Route?)
|
||||
.disabled(!route.isEnabled)
|
||||
}
|
||||
}
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Time Zones",
|
||||
subtitle: "Fetching objects and Querying raw values",
|
||||
destination: {
|
||||
Modern.TimeZonesDemo.MainView()
|
||||
}
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Colors (UIKit)",
|
||||
subtitle: "Observing list changes and single-object changes using DiffableDataSources",
|
||||
destination: {
|
||||
Modern.ColorsDemo.MainView(
|
||||
listView: { listPublisher, onPaletteTapped in
|
||||
Modern.ColorsDemo.UIKit.ListView(
|
||||
listPublisher: listPublisher,
|
||||
onPaletteTapped: onPaletteTapped
|
||||
)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
},
|
||||
detailView: { objectPublisher in
|
||||
Modern.ColorsDemo.UIKit.DetailView(objectPublisher)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Colors (SwiftUI)",
|
||||
subtitle: "Observing list changes and single-object changes using SwiftUI bindings",
|
||||
destination: {
|
||||
Modern.ColorsDemo.MainView(
|
||||
listView: { listPublisher, onPaletteTapped in
|
||||
Modern.ColorsDemo.SwiftUI.ListView(
|
||||
listPublisher: listPublisher,
|
||||
onPaletteTapped: onPaletteTapped
|
||||
)
|
||||
},
|
||||
detailView: { objectPublisher in
|
||||
Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Pokedex API",
|
||||
subtitle: "Importing JSON data from external source",
|
||||
destination: {
|
||||
Modern.PokedexDemo.MainView(
|
||||
listView: Modern.PokedexDemo.UIKit.ListView.init
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Section(header: Text("Classic (NSManagedObject subclasses)")) {
|
||||
Menu.ItemView(
|
||||
title: "Colors",
|
||||
subtitle: "Observing list changes and single-object changes using ListMonitor",
|
||||
destination: {
|
||||
Classic.ColorsDemo.MainView()
|
||||
}
|
||||
)
|
||||
.navigationTitle("CoreStore Demos")
|
||||
.listStyle(.sidebar)
|
||||
},
|
||||
detail: {
|
||||
|
||||
if let selection = self.selection {
|
||||
|
||||
selection.destination
|
||||
}
|
||||
Section(header: Text("Advanced")) {
|
||||
Menu.ItemView(
|
||||
title: "Accounts",
|
||||
subtitle: "Switching between multiple persistent stores",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
.disabled(true)
|
||||
Menu.ItemView(
|
||||
title: "Evolution",
|
||||
subtitle: "Migrating and reverse-migrating stores",
|
||||
destination: {
|
||||
Advanced.EvolutionDemo.MainView()
|
||||
}
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Logger",
|
||||
subtitle: "Implementing a custom logger",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
.disabled(true)
|
||||
else {
|
||||
|
||||
Menu.PlaceholderView()
|
||||
}
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
.navigationBarTitle("CoreStore Demos")
|
||||
Menu.PlaceholderView()
|
||||
}
|
||||
.navigationViewStyle(DoubleColumnNavigationViewStyle())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Menu_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Menu.MainView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,43 +2,28 @@
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
// MARK: - Menu
|
||||
|
||||
extension Menu {
|
||||
|
||||
|
||||
// MARK: - Menu.PlaceholderView
|
||||
|
||||
|
||||
struct PlaceholderView: UIViewControllerRepresentable {
|
||||
|
||||
|
||||
// MARK: UIViewControllerRepresentable
|
||||
|
||||
|
||||
typealias UIViewControllerType = UIViewController
|
||||
|
||||
|
||||
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
|
||||
|
||||
|
||||
return UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()!
|
||||
}
|
||||
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {}
|
||||
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Menu_PlaceholderView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
return Menu.PlaceholderView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,9 +2,202 @@
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
// MARK: - Menu
|
||||
|
||||
enum Menu {}
|
||||
enum Menu {
|
||||
|
||||
// MARK: - Section
|
||||
|
||||
enum Section: String, CaseIterable, Hashable {
|
||||
|
||||
case modern = "Modern (CoreStoreObject subclasses)"
|
||||
case classic = "Classic (NSManagedObject subclasses)"
|
||||
case advanced = "Advanced"
|
||||
|
||||
var routes: [Route] {
|
||||
switch self {
|
||||
|
||||
case .modern:
|
||||
return [
|
||||
.placemarks,
|
||||
.timeZones,
|
||||
.colorsUIKit,
|
||||
.colorsSwiftUI,
|
||||
.pokedex
|
||||
]
|
||||
|
||||
case .classic:
|
||||
return [
|
||||
.classicColors
|
||||
]
|
||||
|
||||
case .advanced:
|
||||
return [
|
||||
.accounts,
|
||||
.evolution,
|
||||
.logger
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Route
|
||||
|
||||
enum Route: String, CaseIterable, Hashable, Identifiable {
|
||||
|
||||
case placemarks
|
||||
case timeZones
|
||||
case colorsUIKit
|
||||
case colorsSwiftUI
|
||||
case pokedex
|
||||
case classicColors
|
||||
case accounts
|
||||
case evolution
|
||||
case logger
|
||||
|
||||
var id: Self {
|
||||
self
|
||||
}
|
||||
|
||||
var title: String {
|
||||
|
||||
switch self {
|
||||
case .placemarks:
|
||||
"Placemarks"
|
||||
|
||||
case .timeZones:
|
||||
"Time Zones"
|
||||
|
||||
case .colorsUIKit:
|
||||
"Colors (UIKit)"
|
||||
|
||||
case .colorsSwiftUI:
|
||||
"Colors (SwiftUI)"
|
||||
|
||||
case .pokedex:
|
||||
"Pokedex API"
|
||||
|
||||
case .classicColors:
|
||||
"Colors"
|
||||
|
||||
case .accounts:
|
||||
"Accounts"
|
||||
|
||||
case .evolution:
|
||||
"Evolution"
|
||||
|
||||
case .logger:
|
||||
"Logger"
|
||||
}
|
||||
}
|
||||
|
||||
var subtitle: String {
|
||||
|
||||
switch self {
|
||||
case .placemarks:
|
||||
"Making changes using Transactions"
|
||||
|
||||
case .timeZones:
|
||||
"Fetching objects and Querying raw values"
|
||||
|
||||
case .colorsUIKit:
|
||||
"Observing list changes and single-object changes using DiffableDataSources"
|
||||
|
||||
case .colorsSwiftUI:
|
||||
"Observing list changes and single-object changes using SwiftUI bindings"
|
||||
|
||||
case .pokedex:
|
||||
"Importing JSON data from external source"
|
||||
|
||||
case .classicColors:
|
||||
"Observing list changes and single-object changes using ListMonitor"
|
||||
|
||||
case .accounts:
|
||||
"Switching between multiple persistent stores"
|
||||
|
||||
case .evolution:
|
||||
"Migrating and reverse-migrating stores"
|
||||
|
||||
case .logger:
|
||||
"Implementing a custom logger"
|
||||
}
|
||||
}
|
||||
|
||||
var isEnabled: Bool {
|
||||
|
||||
switch self {
|
||||
|
||||
case .placemarks,
|
||||
.timeZones,
|
||||
.colorsUIKit,
|
||||
.colorsSwiftUI,
|
||||
.pokedex,
|
||||
.classicColors,
|
||||
.evolution:
|
||||
return true
|
||||
|
||||
case .accounts,
|
||||
.logger:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ViewBuilder
|
||||
var destination: some View {
|
||||
|
||||
switch self {
|
||||
case .placemarks:
|
||||
Modern.PlacemarksDemo.MainView()
|
||||
|
||||
case .timeZones:
|
||||
Modern.TimeZonesDemo.MainView()
|
||||
|
||||
case .colorsUIKit:
|
||||
Modern.ColorsDemo.MainView(
|
||||
listView: { listPublisher, onPaletteTapped in
|
||||
Modern.ColorsDemo.UIKit.ListView(
|
||||
listPublisher: listPublisher,
|
||||
onPaletteTapped: onPaletteTapped
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
},
|
||||
detailView: { objectPublisher in
|
||||
Modern.ColorsDemo.UIKit.DetailView(objectPublisher)
|
||||
}
|
||||
)
|
||||
|
||||
case .colorsSwiftUI:
|
||||
Modern.ColorsDemo.MainView(
|
||||
listView: { listPublisher, onPaletteTapped in
|
||||
Modern.ColorsDemo.SwiftUI.ListView(
|
||||
listPublisher: listPublisher,
|
||||
onPaletteTapped: onPaletteTapped
|
||||
)
|
||||
},
|
||||
detailView: { objectPublisher in
|
||||
Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher)
|
||||
}
|
||||
)
|
||||
|
||||
case .pokedex:
|
||||
Modern.PokedexDemo.MainView(
|
||||
listView: Modern.PokedexDemo.UIKit.ListView.init
|
||||
)
|
||||
|
||||
case .classicColors:
|
||||
Classic.ColorsDemo.MainView()
|
||||
|
||||
case .accounts, .logger:
|
||||
EmptyView()
|
||||
|
||||
case .evolution:
|
||||
Advanced.EvolutionDemo.MainView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user