From 0f3455a4a4a8680d1cba9c3fb44421f700d24799 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Tue, 18 Aug 2020 12:05:20 +0900 Subject: [PATCH] WIP --- Demo/Demo.xcodeproj/project.pbxproj | 86 +++++---- .../Base.lproj/LaunchScreen.storyboard | 25 ++- .../Modern.ColorsDemo.Palette.swift | 2 +- .../Modern.PokedexDemo.MainView.swift | 74 ++++++++ .../Modern.PokedexDemo.PokedexEntry.swift | 59 ++++++ .../Modern.PokedexDemo.PokemonForm.swift | 29 ++- .../Modern.PokedexDemo.PokemonSpecies.swift | 3 +- .../Modern.PokedexDemo.Service.swift | 169 ++++++++++++++++++ .../PokedexDemo/Modern.PokedexDemo.swift | 32 +--- Demo/Sources/Helpers/Menu/Menu.MainView.swift | 18 +- .../Helpers/Menu/Menu.PlaceholderView.swift | 44 +++++ Sources/From+Querying.swift | 54 ++++++ Sources/SectionBy.swift | 66 +++++++ 13 files changed, 557 insertions(+), 104 deletions(-) create mode 100644 Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift create mode 100644 Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift create mode 100644 Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift create mode 100644 Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index b7a05ce..017fb3f 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + B531EFE724EA762D005F247D /* Menu.PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B531EFE624EA762D005F247D /* Menu.PlaceholderView.swift */; }; + B531EFE924EB5A53005F247D /* Modern.PokedexDemo.PokedexEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B531EFE824EB5A52005F247D /* Modern.PokedexDemo.PokedexEntry.swift */; }; + B531EFEB24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = B531EFEA24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift */; }; + B531EFED24EB7453005F247D /* Modern.PokedexDemo.MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B531EFEC24EB7453005F247D /* Modern.PokedexDemo.MainView.swift */; }; B5A3911D24E5429200E7E8BD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3911C24E5429200E7E8BD /* AppDelegate.swift */; }; B5A3911F24E5429200E7E8BD /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */; }; B5A3912124E5429200E7E8BD /* Menu.MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3912024E5429200E7E8BD /* Menu.MainView.swift */; }; @@ -81,6 +85,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + B531EFE624EA762D005F247D /* Menu.PlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.PlaceholderView.swift; sourceTree = ""; }; + B531EFE824EB5A52005F247D /* Modern.PokedexDemo.PokedexEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokedexEntry.swift; sourceTree = ""; }; + B531EFEA24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.Service.swift; sourceTree = ""; }; + B531EFEC24EB7453005F247D /* Modern.PokedexDemo.MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.MainView.swift; sourceTree = ""; }; B5A3911924E5429200E7E8BD /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; B5A3911C24E5429200E7E8BD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -217,6 +225,7 @@ B5A3913324E6170500E7E8BD /* Menu.swift */, B5A3912024E5429200E7E8BD /* Menu.MainView.swift */, B5A3915224E6537F00E7E8BD /* Menu.ItemView.swift */, + B531EFE624EA762D005F247D /* Menu.PlaceholderView.swift */, ); path = Menu; sourceTree = ""; @@ -364,6 +373,8 @@ isa = PBXGroup; children = ( B5A391B024E96AF600E7E8BD /* Modern.PokedexDemo.swift */, + B531EFEA24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift */, + B531EFEC24EB7453005F247D /* Modern.PokedexDemo.MainView.swift */, B5A391B224E96B7400E7E8BD /* Models */, ); path = PokedexDemo; @@ -372,6 +383,7 @@ B5A391B224E96B7400E7E8BD /* Models */ = { isa = PBXGroup; children = ( + B531EFE824EB5A52005F247D /* Modern.PokedexDemo.PokedexEntry.swift */, B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift */, B5A391B824E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift */, B5A391B524E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift */, @@ -460,46 +472,50 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B5A3918824E7A8F900E7E8BD /* Modern.TimeZonesDemo.MainView.swift in Sources */, - B5A391AE24E9150F00E7E8BD /* Modern.ColorsDemo.UIKit.DetailViewController.swift in Sources */, - B5A391A224E8F01F00E7E8BD /* Modern.ColorsDemo.UIKit.ListViewController.swift in Sources */, - B5A391AA24E9104300E7E8BD /* Modern.ColorsDemo.UIKit.ItemCell.swift in Sources */, - B5A3917C24E6A76C00E7E8BD /* LazyView.swift in Sources */, - B5A3918324E7A21800E7E8BD /* Modern.TimeZonesDemo.swift in Sources */, - B5A3915324E6537F00E7E8BD /* Menu.ItemView.swift in Sources */, + B5A391A824E90F1000E7E8BD /* UIImage+Extensions.swift in Sources */, B5A3911D24E5429200E7E8BD /* AppDelegate.swift in Sources */, - B5A391A024E8F00A00E7E8BD /* Modern.ColorsDemo.UIKit.swift in Sources */, - B5A3913424E6170500E7E8BD /* Menu.swift in Sources */, - B5A391B924E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift in Sources */, - B5A3918624E7A54A00E7E8BD /* Modern.TimeZonesDemo.TimeZone.swift in Sources */, - B5A3915B24E685FE00E7E8BD /* Modern.swift in Sources */, - B5A391BB24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift in Sources */, - B5A3919624E7E4AC00E7E8BD /* Modern.ColorsDemo.SwiftUI.ItemView.swift in Sources */, - B5A3919A24E8207A00E7E8BD /* Modern.ColorsDemo.SwiftUI.DetailView.swift in Sources */, - B5A3916024E6925900E7E8BD /* Modern.PlacemarksDemo.MapView.swift in Sources */, - B5A391B124E96AF600E7E8BD /* Modern.PokedexDemo.swift in Sources */, - B5A3918A24E7AD1800E7E8BD /* Modern.TimeZonesDemo.ListView.swift in Sources */, - B5A3911F24E5429200E7E8BD /* SceneDelegate.swift in Sources */, B5A3915924E685EC00E7E8BD /* Classic.swift in Sources */, - B5A3919824E7E67000E7E8BD /* Modern.ColorsDemo.Filter.swift in Sources */, - B5A391B624E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift in Sources */, - B5A3916224E697BA00E7E8BD /* Modern.PlacemarksDemo.MainView.swift in Sources */, - B5A3917E24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift in Sources */, - B5A391A424E8F04300E7E8BD /* Modern.ColorsDemo.UIKit.ListView.swift in Sources */, - B5A391AC24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift in Sources */, - B5A3919224E7E0C600E7E8BD /* Modern.ColorsDemo.Palette.swift in Sources */, - B5A3919424E7E36700E7E8BD /* Modern.ColorsDemo.SwiftUI.ListView.swift in Sources */, + B5A3918024E787D900E7E8BD /* InstructionsView.swift in Sources */, + B5A3917C24E6A76C00E7E8BD /* LazyView.swift in Sources */, + B5A3913424E6170500E7E8BD /* Menu.swift in Sources */, + B5A3915B24E685FE00E7E8BD /* Modern.swift in Sources */, + B5A3911F24E5429200E7E8BD /* SceneDelegate.swift in Sources */, + B5A3915324E6537F00E7E8BD /* Menu.ItemView.swift in Sources */, + B5A3912124E5429200E7E8BD /* Menu.MainView.swift in Sources */, + B531EFE724EA762D005F247D /* Menu.PlaceholderView.swift in Sources */, B5A3918F24E7E06500E7E8BD /* Modern.ColorsDemo.swift in Sources */, B5A3915E24E6922E00E7E8BD /* Modern.PlacemarksDemo.swift in Sources */, - B5A391B424E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift in Sources */, - B5A391A824E90F1000E7E8BD /* UIImage+Extensions.swift in Sources */, - B5A3918024E787D900E7E8BD /* InstructionsView.swift in Sources */, - B5A3918C24E7B44B00E7E8BD /* Modern.TimeZonesDemo.ItemView.swift in Sources */, - B5A3919E24E8EEB600E7E8BD /* Modern.ColorsDemo.SwiftUI.swift in Sources */, + B5A391B124E96AF600E7E8BD /* Modern.PokedexDemo.swift in Sources */, + B5A3918324E7A21800E7E8BD /* Modern.TimeZonesDemo.swift in Sources */, + B531EFEB24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift in Sources */, + B5A3919824E7E67000E7E8BD /* Modern.ColorsDemo.Filter.swift in Sources */, B5A391A624E8F4EA00E7E8BD /* Modern.ColorsDemo.MainView.swift in Sources */, + B5A3919224E7E0C600E7E8BD /* Modern.ColorsDemo.Palette.swift in Sources */, + B5A3919E24E8EEB600E7E8BD /* Modern.ColorsDemo.SwiftUI.swift in Sources */, + B5A391A024E8F00A00E7E8BD /* Modern.ColorsDemo.UIKit.swift in Sources */, + B5A3917E24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift in Sources */, + B5A3916224E697BA00E7E8BD /* Modern.PlacemarksDemo.MainView.swift in Sources */, + B5A3916024E6925900E7E8BD /* Modern.PlacemarksDemo.MapView.swift in Sources */, B5A3916524E698C700E7E8BD /* Modern.PlacemarksDemo.Place.swift in Sources */, B5A391BD24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift in Sources */, - B5A3912124E5429200E7E8BD /* Menu.MainView.swift in Sources */, + B5A391B624E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift in Sources */, + B531EFE924EB5A53005F247D /* Modern.PokedexDemo.PokedexEntry.swift in Sources */, + B5A391B924E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift in Sources */, + B5A391B424E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift in Sources */, + B5A391BB24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift in Sources */, + B5A3918C24E7B44B00E7E8BD /* Modern.TimeZonesDemo.ItemView.swift in Sources */, + B5A3918A24E7AD1800E7E8BD /* Modern.TimeZonesDemo.ListView.swift in Sources */, + B5A3918824E7A8F900E7E8BD /* Modern.TimeZonesDemo.MainView.swift in Sources */, + B531EFED24EB7453005F247D /* Modern.PokedexDemo.MainView.swift in Sources */, + B5A3918624E7A54A00E7E8BD /* Modern.TimeZonesDemo.TimeZone.swift in Sources */, + B5A3919A24E8207A00E7E8BD /* Modern.ColorsDemo.SwiftUI.DetailView.swift in Sources */, + B5A3919624E7E4AC00E7E8BD /* Modern.ColorsDemo.SwiftUI.ItemView.swift in Sources */, + B5A3919424E7E36700E7E8BD /* Modern.ColorsDemo.SwiftUI.ListView.swift in Sources */, + B5A391AC24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift in Sources */, + B5A391AE24E9150F00E7E8BD /* Modern.ColorsDemo.UIKit.DetailViewController.swift in Sources */, + B5A391AA24E9104300E7E8BD /* Modern.ColorsDemo.UIKit.ItemCell.swift in Sources */, + B5A391A424E8F04300E7E8BD /* Modern.ColorsDemo.UIKit.ListView.swift in Sources */, + B5A391A224E8F01F00E7E8BD /* Modern.ColorsDemo.UIKit.ListViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -533,7 +549,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_APPICON_NAME = appIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -607,7 +623,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_APPICON_NAME = appIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; diff --git a/Demo/Resources/Base.lproj/LaunchScreen.storyboard b/Demo/Resources/Base.lproj/LaunchScreen.storyboard index 2b871ce..b3744eb 100644 --- a/Demo/Resources/Base.lproj/LaunchScreen.storyboard +++ b/Demo/Resources/Base.lproj/LaunchScreen.storyboard @@ -3,7 +3,6 @@ - @@ -19,20 +18,24 @@ - - + + + + + + - + @@ -52,12 +55,6 @@ - - - - - - - + diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Palette.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Palette.swift index 9467a32..734a404 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Palette.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Palette.swift @@ -132,7 +132,7 @@ extension Modern.ColorsDemo { private static func randomSaturation() -> Float { - return Float.random(in: 0.0 ... 1.0) + return Float.random(in: 0.4 ... 1.0) } private static func randomBrightness() -> Float { diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift new file mode 100644 index 0000000..8e35b18 --- /dev/null +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift @@ -0,0 +1,74 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import Combine +import CoreStore +import SwiftUI + +// MARK: - Modern.PokedexDemo + +extension Modern.PokedexDemo { + + // MARK: - Modern.PokedexDemo.MainView + + struct MainView: View { + + /** + ⭐️ Sample 1: Setting a sectioned `ListPublisher` declared as an `@ObservedObject` + */ + @ObservedObject + private var pokedexEntries: ListPublisher + + + // MARK: Internal + + init() { + + self.pokedexEntries = Modern.PokedexDemo.pokedexEntries + } + + + // MARK: View + + var body: some View { + List() { + ForEach(self.pokedexEntries.snapshot, id: \.self) { pokedexEntry in + LazyView { + Text(pokedexEntry.snapshot?.$id ?? "") + } + } + } + .overlay( + InstructionsView( + ("Random", "Sets random coordinate"), + ("Tap", "Sets to tapped coordinate") + ) + .padding(.leading, 10) + .padding(.bottom, 40), + alignment: .bottomLeading + ) + .navigationBarTitle("Pokedex") + } + + + // MARK: Private + + private let service: Modern.PokedexDemo.Service = .init() + } +} + + +#if DEBUG + +struct _Demo_Modern_PokedexDemo_MainView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + Modern.PokedexDemo.MainView() + } +} + +#endif diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift new file mode 100644 index 0000000..8516c85 --- /dev/null +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift @@ -0,0 +1,59 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + +// MARK: - Modern.PokedexDemo + +extension Modern.PokedexDemo { + + // MARK: - Modern.PokedexDemo.PokedexEntry + + final class PokedexEntry: CoreStoreObject, ImportableUniqueObject { + + // MARK: Internal + + @Field.Stored("id") + var id: String = "" + + @Field.Stored("url") + var url: URL! + + + @Field.Relationship("form") + var form: Modern.PokedexDemo.PokemonForm? + + + // MARK: ImportableObject + + typealias ImportSource = Dictionary + + + // MARK: ImportableUniqueObject + + static let uniqueIDKeyPath: String = String(keyPath: \Modern.PokedexDemo.PokedexEntry.$id) + + var uniqueIDValue: String { + + get { + + return self.id + } + set { + + self.id = newValue + } + } + + static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> String? { + + return try Modern.PokedexDemo.Service.parseJSON(source["name"]) + } + + func update(from source: ImportSource, in transaction: BaseDataTransaction) throws { + + self.url = URL(string: try Modern.PokedexDemo.Service.parseJSON(source["url"])) + } + } +} diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonForm.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonForm.swift index e0bb63d..49626d5 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonForm.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonForm.swift @@ -19,15 +19,18 @@ extension Modern.PokedexDemo { @Field.Stored("name") var name: String = "" + + @Field.Stored("weight") + var weight: Int = 0 @Field.Stored("pokemonType1") var pokemonType1: Modern.PokedexDemo.PokemonType = .normal @Field.Stored("pokemonType2") var pokemonType2: Modern.PokedexDemo.PokemonType? - - @Field.Relationship("species") - var species: Modern.PokedexDemo.PokemonSpecies? + + @Field.Stored("spriteURL") + var spriteURL: URL? @Field.Stored("statHitPoints") @@ -49,23 +52,17 @@ extension Modern.PokedexDemo { var statSpeed: Int = 0 - @Field.Stored("spriteFrontURL") - var spriteFrontURL: URL? - - @Field.Stored("spriteBackURL") - var spriteBackURL: URL? - - @Field.Stored("spriteShinyFrontURL") - var spriteShinyFrontURL: URL? - - @Field.Stored("spriteShinyBackURL") - var spriteShinyBackURL: URL? - - @Field.Relationship("abilities", inverse: \.$learners) var abilities: Set @Field.Relationship("moves", inverse: \.$learners) var moves: Set + + + @Field.Relationship("pokedexEntry", inverse: \.$form) + var pokedexEntry: Modern.PokedexDemo.PokedexEntry? + + @Field.Relationship("species", inverse: \.$forms) + var species: Modern.PokedexDemo.PokemonSpecies? } } diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonSpecies.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonSpecies.swift index f8da54c..067db1f 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonSpecies.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonSpecies.swift @@ -22,8 +22,9 @@ extension Modern.PokedexDemo { @Field.Stored("weight") var weight: Int = 0 + - @Field.Relationship("forms", inverse: \.$species) + @Field.Relationship("forms") var forms: Set } } diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift new file mode 100644 index 0000000..c8aa98e --- /dev/null +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift @@ -0,0 +1,169 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import Foundation +import Combine +import CoreStore + + +// MARK: - Modern.PokedexDemo + +extension Modern.PokedexDemo { + + // MARK: - Modern.PokedexDemo.Service + + final class Service { + + // MARK: Internal + + @Published + var isLoading: Bool = true + + @Published + var lastError: (error: Modern.PokedexDemo.Service.Error, retry: () -> Void)? + + init() { + + self.fetchPokedexEntries() + } + + static func parseJSON(_ json: Any?) throws -> Output { + + switch json { + + case let json as Output: + return json + + case let any: + throw Modern.PokedexDemo.Service.Error.parseError( + expected: Output.self, + actual: type(of: any) + ) + } + } + + func fetchPokedexEntries() { + + self.cancellable["pokedexEntries"] = self.pokedexEntries + .handleEvents( + receiveSubscription: { [weak self] _ in + + print("Fetching Pokedex Entries") + guard let self = self else { + + return + } + self.lastError = nil + self.isLoading = true + } + ) + .sink( + receiveCompletion: { [weak self] completion in + + print("Result (Fetching Pokedex Entries): \(completion)") + guard let self = self else { + + return + } + self.isLoading = false + switch completion { + + case .finished: + self.lastError = nil + + case .failure(let error): + self.lastError = ( + error: error, + retry: { [weak self] in + + self?.fetchPokedexEntries() + } + ) + } + }, + receiveValue: {} + ) + } + + func fetchPokemonForm(for pokedexEntry: ObjectSnapshot) { + + self.cancellable["pokedexEntry.\(pokedexEntry.$id)"] = URLSession.shared + .dataTaskPublisher(for: pokedexEntry.$url!) + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + .sink( + receiveCompletion: { _ in }, + receiveValue: { _ in + + } + ) + } + + + // MARK: Private + + private var cancellable: Dictionary = [:] + + private lazy var pokedexEntries: AnyPublisher = URLSession.shared + .dataTaskPublisher( + for: URL(string: "https://pokeapi.co/api/v2/pokemon?limit=10000&offset=0")! + ) + .mapError({ .networkError($0) }) + .flatMap( + { output in + + return Future { promise in + + do { + + let json: Dictionary = try Self.parseJSON( + try JSONSerialization.jsonObject(with: output.data, options: []) + ) + let results: [Dictionary] = try Self.parseJSON( + json["results"] + ) + Modern.PokedexDemo.dataStack.perform( + asynchronous: { transaction -> Void in + + _ = try transaction.importUniqueObjects( + Into(), + sourceArray: results + ) + }, + success: { result in + + promise(.success(result)) + }, + failure: { error in + + promise(.failure(.saveError(error))) + } + ) + } + catch let error as Modern.PokedexDemo.Service.Error { + + promise(.failure(error)) + } + catch { + + promise(.failure(.otherError(error))) + } + } + } + ) + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + + + // MARK: - Modern.PokedexDemo.Service.Error + + enum Error: Swift.Error { + + case networkError(URLError) + case parseError(expected: Any.Type, actual: Any.Type) + case saveError(CoreStoreError) + case otherError(Swift.Error) + } + } +} diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift index 95c9fab..1bebdef 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift @@ -23,10 +23,11 @@ extension Modern { CoreStoreSchema( modelVersion: "V1", entities: [ - Entity("Palette") - ], - versionLock: [ - "Palette": [0xbaf4eaee9353176a, 0xdd6ca918cc2b0c38, 0xd04fad8882d7cc34, 0x3e90ca38c091503f] + Entity("PokedexEntry"), + Entity("PokemonSpecies"), + Entity("PokemonForm"), + Entity("Move"), + Entity("Ability") ] ) ) @@ -36,31 +37,16 @@ extension Modern { */ try! dataStack.addStorageAndWait( SQLiteStore( - fileName: "Modern.ColorsDemo.sqlite", + fileName: "Modern.PokedexDemo.sqlite", localStorageOptions: .recreateStoreOnModelMismatch ) ) return dataStack }() - static let palettesPublisher: ListPublisher = Modern.ColorsDemo.dataStack.publishList( - From() - .sectionBy(\.$colorName) - .where(Modern.ColorsDemo.filter.whereClause()) - .orderBy(.ascending(\.$hue)) + static let pokedexEntries: ListPublisher = Modern.PokedexDemo.dataStack.publishList( + From() + .orderBy(.ascending(\.$id)) ) - - static var filter: Modern.ColorsDemo.Filter = .all { - - didSet { - - try! Modern.ColorsDemo.palettesPublisher.refetch( - From() - .sectionBy(\.$colorName) - .where(self.filter.whereClause()) - .orderBy(.ascending(\.$hue)) - ) - } - } } } diff --git a/Demo/Sources/Helpers/Menu/Menu.MainView.swift b/Demo/Sources/Helpers/Menu/Menu.MainView.swift index d7fbf79..a16436a 100644 --- a/Demo/Sources/Helpers/Menu/Menu.MainView.swift +++ b/Demo/Sources/Helpers/Menu/Menu.MainView.swift @@ -72,7 +72,9 @@ extension Menu { Menu.ItemView( title: "Pokedex API", subtitle: "Importing JSON data from external source", - destination: { EmptyView() } + destination: { + Modern.PokedexDemo.MainView() + } ) } Section(header: Text("Classic (NSManagedObject subclasses)")) { @@ -127,23 +129,11 @@ extension Menu { } .listStyle(GroupedListStyle()) .navigationBarTitle("CoreStore Demos") - Menu.DetailView() + Menu.PlaceholderView() } .navigationViewStyle(DoubleColumnNavigationViewStyle()) } } - - fileprivate struct DetailView: View { - - var selectedDate: Date? - - var body: some View { - Group { - Text("Detail view content goes here") - } - .navigationBarTitle(Text("Detail")) - } - } } #if DEBUG diff --git a/Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift b/Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift new file mode 100644 index 0000000..9618327 --- /dev/null +++ b/Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift @@ -0,0 +1,44 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import Combine +import CoreStore +import SwiftUI + +// MARK: - Menu + +extension Menu { + + // MARK: - Menu.PlaceholderView + + struct PlaceholderView: UIViewControllerRepresentable { + + // MARK: UIViewControllerRepresentable + + typealias UIViewControllerType = UIViewController + + func makeUIViewController(context: Self.Context) -> UIViewControllerType { + + return UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()! + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {} + + static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {} + } +} + +#if DEBUG + +struct _Demo_Menu_PlaceholderView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + return Menu.PlaceholderView() + } +} + +#endif diff --git a/Sources/From+Querying.swift b/Sources/From+Querying.swift index 2b4c597..77eca5a 100644 --- a/Sources/From+Querying.swift +++ b/Sources/From+Querying.swift @@ -368,6 +368,18 @@ extension From where O: CoreStoreObject { return self.sectionBy(O.meta[keyPath: sectionKeyPath].keyPath, { $0 }) } + + /** + Creates a `SectionMonitorChainBuilder` with the key path to use to group `ListMonitor` objects into sections + + - parameter sectionKeyPath: the `KeyPath` to use to group the objects into sections + - returns: a `SectionMonitorChainBuilder` that is sectioned by the specified key path + */ + @available(macOS 10.12, *) + public func sectionBy(_ sectionKeyPath: KeyPath.Coded>) -> SectionMonitorChainBuilder { + + return self.sectionBy(O.meta[keyPath: sectionKeyPath].keyPath, { $0 }) + } /** Creates a `SectionMonitorChainBuilder` with the key path to use to group `ListMonitor` objects into sections @@ -416,6 +428,48 @@ extension From where O: CoreStoreObject { return self.sectionBy(O.meta[keyPath: sectionKeyPath].keyPath, { $0 }) } + + /** + Creates a `SectionMonitorChainBuilder` with the key path to use to group `ListMonitor` objects into sections, and a closure to transform the value for the key path to an appropriate section name + + - Important: Some utilities (such as `ListMonitor`s) may keep `SectionBy`s in memory and may thus introduce retain cycles if reference captures are not handled properly. + - parameter sectionKeyPath: the `KeyPath` to use to group the objects into sections + - parameter sectionIndexTransformer: a closure to transform the value for the key path to an appropriate section name + - returns: a `SectionMonitorChainBuilder` that is sectioned by the specified key path + */ + @available(macOS 10.12, *) + public func sectionBy(_ sectionKeyPath: KeyPath.Stored>, _ sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?) -> SectionMonitorChainBuilder { + + return self.sectionBy(O.meta[keyPath: sectionKeyPath].keyPath, sectionIndexTransformer) + } + + /** + Creates a `SectionMonitorChainBuilder` with the key path to use to group `ListMonitor` objects into sections, and a closure to transform the value for the key path to an appropriate section name + + - Important: Some utilities (such as `ListMonitor`s) may keep `SectionBy`s in memory and may thus introduce retain cycles if reference captures are not handled properly. + - parameter sectionKeyPath: the `KeyPath` to use to group the objects into sections + - parameter sectionIndexTransformer: a closure to transform the value for the key path to an appropriate section name + - returns: a `SectionMonitorChainBuilder` that is sectioned by the specified key path + */ + @available(macOS 10.12, *) + public func sectionBy(_ sectionKeyPath: KeyPath.Virtual>, _ sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?) -> SectionMonitorChainBuilder { + + return self.sectionBy(O.meta[keyPath: sectionKeyPath].keyPath, sectionIndexTransformer) + } + + /** + Creates a `SectionMonitorChainBuilder` with the key path to use to group `ListMonitor` objects into sections, and a closure to transform the value for the key path to an appropriate section name + + - Important: Some utilities (such as `ListMonitor`s) may keep `SectionBy`s in memory and may thus introduce retain cycles if reference captures are not handled properly. + - parameter sectionKeyPath: the `KeyPath` to use to group the objects into sections + - parameter sectionIndexTransformer: a closure to transform the value for the key path to an appropriate section name + - returns: a `SectionMonitorChainBuilder` that is sectioned by the specified key path + */ + @available(macOS 10.12, *) + public func sectionBy(_ sectionKeyPath: KeyPath.Coded>, _ sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?) -> SectionMonitorChainBuilder { + + return self.sectionBy(O.meta[keyPath: sectionKeyPath].keyPath, sectionIndexTransformer) + } /** Creates a `SectionMonitorChainBuilder` with the key path to use to group `ListMonitor` objects into sections, and a closure to transform the value for the key path to an appropriate section name diff --git a/Sources/SectionBy.swift b/Sources/SectionBy.swift index 3a0b662..80dad6e 100644 --- a/Sources/SectionBy.swift +++ b/Sources/SectionBy.swift @@ -106,6 +106,36 @@ extension SectionBy where O: NSManagedObject { @available(macOS 10.12, *) extension SectionBy where O: CoreStoreObject { + + /** + Initializes a `SectionBy` clause with the key path to use to group `ListMonitor` objects into sections + + - parameter sectionKeyPath: the key path to use to group the objects into sections + */ + public init(_ sectionKeyPath: KeyPath.Stored>) { + + self.init(sectionKeyPath, { $0 }) + } + + /** + Initializes a `SectionBy` clause with the key path to use to group `ListMonitor` objects into sections + + - parameter sectionKeyPath: the key path to use to group the objects into sections + */ + public init(_ sectionKeyPath: KeyPath.Virtual>) { + + self.init(sectionKeyPath, { $0 }) + } + + /** + Initializes a `SectionBy` clause with the key path to use to group `ListMonitor` objects into sections + + - parameter sectionKeyPath: the key path to use to group the objects into sections + */ + public init(_ sectionKeyPath: KeyPath.Coded>) { + + self.init(sectionKeyPath, { $0 }) + } /** Initializes a `SectionBy` clause with the key path to use to group `ListMonitor` objects into sections @@ -158,6 +188,42 @@ extension SectionBy where O: CoreStoreObject { self.init(O.meta[keyPath: sectionKeyPath].keyPath, sectionIndexTransformer) } + + /** + Initializes a `SectionBy` clause with the key path to use to group `ListMonitor` objects into sections, and a closure to transform the value for the key path to an appropriate section name + + - Important: Some utilities (such as `ListMonitor`s) may keep `SectionBy`s in memory and may thus introduce retain cycles if reference captures are not handled properly. + - parameter sectionKeyPath: the key path to use to group the objects into sections + - parameter sectionIndexTransformer: a closure to transform the value for the key path to an appropriate section name + */ + public init(_ sectionKeyPath: KeyPath.Stored>, _ sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?) { + + self.init(O.meta[keyPath: sectionKeyPath].keyPath, sectionIndexTransformer) + } + + /** + Initializes a `SectionBy` clause with the key path to use to group `ListMonitor` objects into sections, and a closure to transform the value for the key path to an appropriate section name + + - Important: Some utilities (such as `ListMonitor`s) may keep `SectionBy`s in memory and may thus introduce retain cycles if reference captures are not handled properly. + - parameter sectionKeyPath: the key path to use to group the objects into sections + - parameter sectionIndexTransformer: a closure to transform the value for the key path to an appropriate section name + */ + public init(_ sectionKeyPath: KeyPath.Virtual>, _ sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?) { + + self.init(O.meta[keyPath: sectionKeyPath].keyPath, sectionIndexTransformer) + } + + /** + Initializes a `SectionBy` clause with the key path to use to group `ListMonitor` objects into sections, and a closure to transform the value for the key path to an appropriate section name + + - Important: Some utilities (such as `ListMonitor`s) may keep `SectionBy`s in memory and may thus introduce retain cycles if reference captures are not handled properly. + - parameter sectionKeyPath: the key path to use to group the objects into sections + - parameter sectionIndexTransformer: a closure to transform the value for the key path to an appropriate section name + */ + public init(_ sectionKeyPath: KeyPath.Coded>, _ sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?) { + + self.init(O.meta[keyPath: sectionKeyPath].keyPath, sectionIndexTransformer) + } /** Initializes a `SectionBy` clause with the key path to use to group `ListMonitor` objects into sections, and a closure to transform the value for the key path to an appropriate section name