|
// |
|
// ContentView.swift |
|
// temp |
|
// |
|
// Created by beader on 2023/2/8. |
|
// |
|
|
|
import SwiftUI |
|
import CoreData |
|
import Combine |
|
import Charts |
|
|
|
struct FirstAppear: ViewModifier { |
|
// The action will be executed the first time the view appears |
|
let action: () -> () |
|
@State private var hasAppeared = false |
|
|
|
func body(content: Content) -> some View { |
|
content.task { |
|
guard !hasAppeared else { return } |
|
hasAppeared = true |
|
action() |
|
} |
|
} |
|
} |
|
|
|
extension View { |
|
func onFirstAppear(_ action: @escaping () -> ()) -> some View { |
|
modifier(FirstAppear(action: action)) |
|
} |
|
} |
|
|
|
struct FetchedResultsView<FetchedEntity: NSManagedObject, Content: View>: View, Equatable { |
|
// FetchedResultsView will be re-rendered only when the sortDescriptors or predicate changed |
|
static func == (lhs: FetchedResultsView<FetchedEntity, Content>, rhs: FetchedResultsView<FetchedEntity, Content>) -> Bool { |
|
(lhs.request.sortDescriptors == rhs.request.sortDescriptors) && |
|
(lhs.request.predicate == rhs.request.predicate) |
|
} |
|
|
|
typealias Request = NSFetchRequest<FetchedEntity> |
|
typealias Results = FetchedResults<FetchedEntity> |
|
|
|
let content: (Results) -> Content |
|
let request: Request |
|
@FetchRequest var results: Results |
|
|
|
init( |
|
request: Request, |
|
@ViewBuilder content: @escaping (Results) -> Content |
|
) { |
|
self.content = content |
|
self.request = request |
|
_results = FetchRequest(fetchRequest: request, animation: .default) |
|
} |
|
|
|
var body: some View { |
|
let _ = print("FetchedResultsView: @self changed.") |
|
content(results) |
|
} |
|
} |
|
|
|
struct FetchRequestViewModifier<FetchedEntity: NSManagedObject>: ViewModifier { |
|
typealias Request = NSFetchRequest<FetchedEntity> |
|
typealias Results = FetchedResults<FetchedEntity> |
|
|
|
let request: Request |
|
let onChange: (Results) -> Void |
|
|
|
func body(content: Content) -> some View { |
|
content.background( |
|
FetchedResultsView(request: request) { results in |
|
Color.clear |
|
.onFirstAppear { |
|
onChange(results) |
|
} |
|
.id(UUID()) |
|
} |
|
) |
|
} |
|
} |
|
|
|
extension View { |
|
func doFetchRequest<FetchedEntity: NSManagedObject>( |
|
request: NSFetchRequest<FetchedEntity>, |
|
onChange: @escaping (FetchedResults<FetchedEntity>) -> Void |
|
) -> some View { |
|
modifier(FetchRequestViewModifier(request: request, onChange: onChange)) |
|
} |
|
} |
|
|
|
struct ItemList<Data>: View where Data: RandomAccessCollection, Data.Element == Item, Data.Index == Int { |
|
|
|
let items: Data? |
|
@Environment(\.managedObjectContext) private var viewContext |
|
|
|
var body: some View { |
|
let _ = print("ItemListView: @self changed.") |
|
if let items { |
|
rows(for: items) |
|
} else { |
|
emptyView |
|
} |
|
} |
|
|
|
private var emptyView: some View { |
|
Text("empty") |
|
} |
|
|
|
@ViewBuilder |
|
private func rows(for items: Data) -> some View { |
|
Section { |
|
ForEach(items) { item in |
|
ItemRow(item: item) |
|
} |
|
.onDelete(perform: onDelete) |
|
} |
|
} |
|
|
|
private func onDelete(_ indexSet: IndexSet) { |
|
guard let items else { return } |
|
indexSet.map { items[$0] }.forEach { item in |
|
viewContext.delete(item) |
|
} |
|
do { |
|
try viewContext.save() |
|
} catch { |
|
print("\(error.localizedDescription)") |
|
} |
|
} |
|
} |
|
|
|
struct ItemRow: View { |
|
@ObservedObject var item: Item |
|
var body: some View { |
|
let _ = Self._printChanges() |
|
NavigationLink { |
|
ItemEditView(item: item) |
|
} label: { |
|
VStack(alignment: .leading) { |
|
Text("\(item.timestamp?.formatted() ?? "")") |
|
Text("\(item.price.formatted(.number.precision(.fractionLength(2))))") |
|
} |
|
} |
|
} |
|
} |
|
|
|
struct DistributionChart: View { |
|
typealias BinnedDataItem = (index: Int, range: ChartBinRange<Double>, frequency: Int) |
|
let binnedData: [BinnedDataItem] |
|
|
|
var body: some View { |
|
let _ = Self._printChanges() |
|
Chart(binnedData, id: \.index) { element in |
|
BarMark( |
|
x: .value("Price", element.range), |
|
y: .value("Frequency", element.frequency) |
|
) |
|
} |
|
.chartXScale( |
|
domain: .automatic(includesZero: false) |
|
) |
|
} |
|
} |
|
|
|
|
|
struct ContentView: View { |
|
@State var count: Int = 0 |
|
@State var minPrice: Double = 0 |
|
@State var averagePrice: Double = 0 |
|
@State var updating: Bool = false |
|
@State var ascending: Bool = true |
|
@State var items: FetchedResults<Item>? = nil |
|
@State var binnedData: [DistributionChart.BinnedDataItem] = [] |
|
|
|
var request: NSFetchRequest<Item> { |
|
let request = Item.fetchRequest() |
|
request.predicate = NSPredicate(format: "price >= %f", minPrice) |
|
request.sortDescriptors = [ |
|
NSSortDescriptor(keyPath: \Item.timestamp, ascending: ascending) |
|
] |
|
return request |
|
} |
|
|
|
private func updateAveragePrice<Data>(items: Data) where Data: Collection, Data.Element == Item { |
|
print("Calculate Average Price...") |
|
updating = true |
|
guard !items.isEmpty else { |
|
averagePrice = 0 |
|
return |
|
} |
|
averagePrice = items.map(\.price).reduce(0, +) / Double(items.count) |
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { |
|
updating = false |
|
} |
|
} |
|
|
|
private func updateBinnedData<Data>(items: Data) where Data: Collection, Data.Element == Item { |
|
print("Updating binned data") |
|
let prices = items.map(\.price) |
|
let bins = NumberBins(data: prices, desiredCount: 5) |
|
let groups = Dictionary(grouping: prices, by: bins.index) |
|
let preparedData = groups.map { key, values in |
|
return ( |
|
index: key, |
|
range: bins[key], |
|
frequency: values.count |
|
) |
|
} |
|
withAnimation { |
|
binnedData = preparedData |
|
|
|
} |
|
} |
|
|
|
var body: some View { |
|
let _ = Self._printChanges() |
|
NavigationView { |
|
List { |
|
DistributionChart(binnedData: binnedData) |
|
.frame(height: 200) |
|
ItemList(items: items) |
|
} |
|
.doFetchRequest(request: request, onChange: { items in |
|
self.items = items |
|
updateAveragePrice(items: items) |
|
updateBinnedData(items: items) |
|
}) |
|
.navigationTitle("Items") |
|
.toolbar { |
|
toolbar |
|
} |
|
} |
|
} |
|
|
|
@ToolbarContentBuilder |
|
var toolbar: some ToolbarContent { |
|
ToolbarItem(placement: .bottomBar) { |
|
VStack { |
|
Slider(value: $minPrice, in: 0...10, step: 1.0) { |
|
Text("minimum price") |
|
} minimumValueLabel: { |
|
Text("0") |
|
} maximumValueLabel: { |
|
Text("10") |
|
} |
|
HStack { |
|
Text("Items with price >= \(minPrice.formatted(.number.precision(.fractionLength(0))))") |
|
Text("Average Price: ") |
|
if updating { |
|
ProgressView() |
|
} else { |
|
Text("\(averagePrice.formatted(.number.precision(.fractionLength(2))))") |
|
} |
|
} |
|
.frame(maxWidth: .infinity) |
|
} |
|
} |
|
ToolbarItem(placement: .navigationBarLeading) { |
|
Button { |
|
count += 1 |
|
} label: { |
|
Text("You Tapped \(count) times") |
|
} |
|
|
|
} |
|
ToolbarItemGroup { |
|
Button { |
|
ascending = !ascending |
|
} label: { |
|
Image(systemName: "arrow.up.arrow.down") |
|
} |
|
} |
|
|
|
ToolbarItem { |
|
NavigationLink { |
|
ItemEditView() |
|
} label: { |
|
Label("Add Item", systemImage: "plus") |
|
} |
|
} |
|
} |
|
} |
|
|
|
struct ContentView_Previews: PreviewProvider { |
|
static var previews: some View { |
|
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) |
|
} |
|
} |
fetchrequest_demo.mp4