From 2c0cadf2fa77b6e4aa62edd63bb558355a1dae75 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 19 Aug 2020 18:49:08 +0900 Subject: [PATCH] WIP --- Demo/Demo.xcodeproj/project.pbxproj | 4 + .../Modern.PokedexDemo.ItemView.swift | 96 +++++++++++++++++++ .../Modern.PokedexDemo.MainView.swift | 77 ++++++++++----- .../Modern.PokedexDemo.PokedexEntry.swift | 36 ++++--- .../Modern.PokedexDemo.Service.swift | 68 +++++++++++-- .../PokedexDemo/Modern.PokedexDemo.swift | 2 +- .../Modern.TimeZonesDemo.ItemView.swift | 2 +- 7 files changed, 229 insertions(+), 56 deletions(-) create mode 100644 Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ItemView.swift diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 017fb3f..265640e 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 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 */; }; + B54269C624ED190C00A66C23 /* Modern.PokedexDemo.ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54269C524ED190C00A66C23 /* Modern.PokedexDemo.ItemView.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 */; }; @@ -89,6 +90,7 @@ 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 = ""; }; + B54269C524ED190C00A66C23 /* Modern.PokedexDemo.ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.ItemView.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 = ""; }; @@ -375,6 +377,7 @@ B5A391B024E96AF600E7E8BD /* Modern.PokedexDemo.swift */, B531EFEA24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift */, B531EFEC24EB7453005F247D /* Modern.PokedexDemo.MainView.swift */, + B54269C524ED190C00A66C23 /* Modern.PokedexDemo.ItemView.swift */, B5A391B224E96B7400E7E8BD /* Models */, ); path = PokedexDemo; @@ -499,6 +502,7 @@ B5A3916524E698C700E7E8BD /* Modern.PlacemarksDemo.Place.swift in Sources */, B5A391BD24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift in Sources */, B5A391B624E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift in Sources */, + B54269C624ED190C00A66C23 /* Modern.PokedexDemo.ItemView.swift in Sources */, B531EFE924EB5A53005F247D /* Modern.PokedexDemo.PokedexEntry.swift in Sources */, B5A391B924E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift in Sources */, B5A391B424E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift in Sources */, diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ItemView.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ItemView.swift new file mode 100644 index 0000000..257a6d6 --- /dev/null +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ItemView.swift @@ -0,0 +1,96 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import SwiftUI + +// MARK: - Modern.PokedexDemo + +extension Modern.PokedexDemo { + + // MARK: - Modern.PokedexDemo.ItemView + + struct ItemView: View { + + // MARK: Internal + + static let preferredHeight: CGFloat = 100 + + init( + pokedexEntry: ObjectPublisher, + service: Modern.PokedexDemo.Service + ) { + + self.pokedexEntry = pokedexEntry + self.service = service + } + + + // MARK: View + + var body: some View { + + let pokedexEntry = self.pokedexEntry.snapshot + let form = pokedexEntry?.$form + let placeholderColor = Color.init(.sRGB, white: 0.95, opacity: 1) + return HStack(spacing: 10) { + placeholderColor + .frame(width: 70, height: 70) + .cornerRadius(10) + + Text(form?.$name ?? pokedexEntry?.$id ?? "") + .foregroundColor(form == nil ? placeholderColor : .init(.darkText)) + .fontWeight(form == nil ? .heavy : .regular) + .frame(maxWidth: .infinity) + } + .padding() + .onAppear { + + if let pokedexEntry = pokedexEntry, form == nil { + + self.service.fetchPokemonForm(for: pokedexEntry) + } + } + } + + + // MARK: Private + + @ObservedObject + private var pokedexEntry: ObjectPublisher + + private let service: Modern.PokedexDemo.Service + } +} + +#if DEBUG + +struct _Demo_Modern_PokedexDemo_ItemView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static let service = Modern.PokedexDemo.Service() + + static var previews: some View { + + try! Modern.PokedexDemo.dataStack.perform( + synchronous: { transaction in + + guard (try transaction.fetchCount(From())) <= 0 else { + return + } + let pokedexEntry = transaction.create(Into()) + pokedexEntry.id = "bulbasaur" + pokedexEntry.url = URL(string: "https://pokeapi.co/api/v2/pokemon/1/")! + } + ) + + return Modern.PokedexDemo.ItemView( + pokedexEntry: Modern.PokedexDemo.pokedexEntries.snapshot.first!, + service: Modern.PokedexDemo.Service() + ) + } +} + +#endif diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift index cdf100b..ea5fceb 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift @@ -32,33 +32,58 @@ extension Modern.PokedexDemo { // MARK: View var body: some View { - ZStack { - ScrollView { - ForEach(self.pokedexEntries.snapshot.prefix(self.visibleItems), id: \.self) { pokedexEntry in - LazyView { - Text(pokedexEntry.snapshot?.$name ?? "") - } - .frame(height: 100) - .frame(minWidth: 0, maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/) - } - Button( - action: { - self.visibleItems = min( - self.visibleItems + 50, - self.pokedexEntries.snapshot.count - ) - }, - label: { Text("Load more") } - ) - } - if self.service.isLoading { - Color(.sRGB, white: 0, opacity: 0.3) - .overlay( + let pokedexEntries = self.pokedexEntries.snapshot + let visibleItems = self.visibleItems + return ZStack { + + if pokedexEntries.isEmpty { + + VStack(alignment: .center, spacing: 20) { + Text("This demo needs to make a network connection to download Pokedex entries") + if self.service.isLoading { + Text("Fetching Pokedex…") - .foregroundColor(.white), - alignment: .center - ) - .edgesIgnoringSafeArea(.bottom) + } + else { + + Button( + action: { self.service.fetchPokedexEntries() }, + label: { + + Text("Download Pokedex Entries") + } + ) + } + } + .padding() + } + else { + + List { + + ForEach(0 ..< min(visibleItems, pokedexEntries.count), id: \.self) { index in + LazyView { + Modern.PokedexDemo.ItemView( + pokedexEntry: pokedexEntries[index], + service: self.service + ) + } + .frame(height: Modern.PokedexDemo.ItemView.preferredHeight) + .frame(minWidth: 0, maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/) + } + if visibleItems < pokedexEntries.count { + + Spacer(minLength: Modern.PokedexDemo.ItemView.preferredHeight) + .onAppear { + + self.visibleItems = min( + visibleItems + 50, + pokedexEntries.count + ) + } + } + } + .id(pokedexEntries) } } .navigationBarTitle("Pokedex") diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift index 4ed9156..e618bdc 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift @@ -13,15 +13,18 @@ extension Modern.PokedexDemo { final class PokedexEntry: CoreStoreObject, ImportableUniqueObject { // MARK: Internal + + @Field.Stored("index") + var index: Int = 0 @Field.Stored("id") - var id: Int = 0 + var id: String = "" - @Field.Stored("name") - var name: String = "" - - @Field.Stored("url") - var url: URL! + @Field.Stored( + "url", + dynamicInitialValue: { URL(string: "data:application/json,%7B%7D")! } + ) + var url: URL @Field.Relationship("form") @@ -35,32 +38,27 @@ extension Modern.PokedexDemo { // MARK: ImportableUniqueObject - typealias UniqueIDType = Int + typealias UniqueIDType = String static let uniqueIDKeyPath: String = String(keyPath: \Modern.PokedexDemo.PokedexEntry.$id) var uniqueIDValue: UniqueIDType { - get { - - return self.id - } - set { - - self.id = newValue - } + get { return self.id } + set { self.id = newValue } } - static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType? { + static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> String? { - return source.index + 1 + let json = source.json + return try Modern.PokedexDemo.Service.parseJSON(json["name"]) } func update(from source: ImportSource, in transaction: BaseDataTransaction) throws { let json = source.json - self.name = try Modern.PokedexDemo.Service.parseJSON(json["name"]) - self.url = URL(string: try Modern.PokedexDemo.Service.parseJSON(json["url"])) + self.index = source.index + self.url = try Modern.PokedexDemo.Service.parseJSON(json["url"], transformer: URL.init(string:)) } } } diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift index b3a5b6e..959f4b7 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift @@ -17,7 +17,7 @@ extension Modern.PokedexDemo { // MARK: Internal - private(set) var isLoading: Bool = true { + private(set) var isLoading: Bool = false { willSet { @@ -33,10 +33,7 @@ extension Modern.PokedexDemo { } } - init() { - - self.fetchPokedexEntries() - } + init() {} static func parseJSON(_ json: Any?) throws -> Output { @@ -53,6 +50,29 @@ extension Modern.PokedexDemo { } } + static func parseJSON(_ json: Any?, transformer: (JSONType) -> Output?) throws -> Output { + + switch json { + + case let json as JSONType: + let transformed = transformer(json) + if let json = transformed { + + return json + } + throw Modern.PokedexDemo.Service.Error.parseError( + expected: Output.self, + actual: type(of: transformed) + ) + + case let any: + throw Modern.PokedexDemo.Service.Error.parseError( + expected: Output.self, + actual: type(of: any) + ) + } + } + func fetchPokedexEntries() { self.cancellable["pokedexEntries"] = self.pokedexEntries @@ -96,14 +116,44 @@ extension Modern.PokedexDemo { func fetchPokemonForm(for pokedexEntry: ObjectSnapshot) { - self.cancellable["pokedexEntry.\(pokedexEntry.$id)"] = URLSession.shared - .dataTaskPublisher(for: pokedexEntry.$url!) - .receive(on: DispatchQueue.main) + self.cancellable["pokemonForm.\(pokedexEntry.$id)"] = URLSession.shared + .dataTaskPublisher(for: pokedexEntry.$url) .eraseToAnyPublisher() .sink( receiveCompletion: { _ in }, - receiveValue: { _ in + receiveValue: { output 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.enumerated().map { (index, json) in +// (index: index, json: json) +// } +// ) +// }, +// success: { result in +// +// promise(.success(result)) +// }, +// failure: { error in +// +// promise(.failure(.saveError(error))) +// } +// ) +// } +// catch { +// +// } } ) } diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift index 1bebdef..f238b9c 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift @@ -46,7 +46,7 @@ extension Modern { static let pokedexEntries: ListPublisher = Modern.PokedexDemo.dataStack.publishList( From() - .orderBy(.ascending(\.$id)) + .orderBy(.ascending(\.$index)) ) } } diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift index 809a450..6361938 100644 --- a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift +++ b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift @@ -43,7 +43,7 @@ extension Modern.TimeZonesDemo { #if DEBUG -struct _Demo_Modern_TimeZone_ItemView_Preview: PreviewProvider { +struct _Demo_Modern_TimeZonesDemo_ItemView_Preview: PreviewProvider { // MARK: PreviewProvider