Displaying history

This commit is contained in:
Selim Mustafaev 2022-03-28 20:10:47 +03:00
parent 49046d5619
commit aa7efbe1b0
9 changed files with 324 additions and 47 deletions

View File

@ -18,6 +18,8 @@
6841A8FF53F0AADF96B138C1 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6841AFE790F6FC06838B1E2C /* UIControl.swift */; };
6841ABD5E4B126DEF3612BBD /* PNKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6841AB0052E9DB6914901EA3 /* PNKeyboard.swift */; };
6841AF924E165F1B3A3B5FB5 /* AuthController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6841ABEA0314E3B4E438C311 /* AuthController.swift */; };
7A1D80E027F1F275007BD64F /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7A1D80DF27F1F275007BD64F /* DifferenceKit */; };
7A1D80E627F20FCB007BD64F /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7A1D80E527F20FCB007BD64F /* DifferenceKit */; };
7A24C19727EE212E00049E7F /* RoadNumbers.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7A24C19527EE212E00049E7F /* RoadNumbers.otf */; };
7A24C19827EE212E00049E7F /* RoadNumbers2.0.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7A24C19627EE212E00049E7F /* RoadNumbers2.0.otf */; };
7A24C19C27EE25B400049E7F /* PlateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A24C19A27EE25B400049E7F /* PlateView.swift */; };
@ -183,6 +185,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7A1D80E027F1F275007BD64F /* DifferenceKit in Frameworks */,
7A49F4EC27D4064500AEAAE0 /* AutoCatCore.framework in Frameworks */,
7A48B26727D9442A004D1A4B /* PKHUD in Frameworks */,
7A28283627E74C110049BDBF /* SwiftEntryKit in Frameworks */,
@ -207,6 +210,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7A1D80E627F20FCB007BD64F /* DifferenceKit in Frameworks */,
7AE32D6E27F06D2D004EF6E0 /* SwiftDate in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -477,6 +481,7 @@
packageProductDependencies = (
7A48B26627D9442A004D1A4B /* PKHUD */,
7A28283527E74C110049BDBF /* SwiftEntryKit */,
7A1D80DF27F1F275007BD64F /* DifferenceKit */,
);
productName = AutoCat2;
productReference = 7A49F49F27D4061900AEAAE0 /* AutoCat2.app */;
@ -534,6 +539,7 @@
name = AutoCatCore;
packageProductDependencies = (
7AE32D6D27F06D2D004EF6E0 /* SwiftDate */,
7A1D80E527F20FCB007BD64F /* DifferenceKit */,
);
productName = AutoCatCore;
productReference = 7A49F4D727D4064500AEAAE0 /* AutoCatCore.framework */;
@ -601,6 +607,8 @@
7A48B26527D9442A004D1A4B /* XCRemoteSwiftPackageReference "PKHUD" */,
7A28283427E74C110049BDBF /* XCRemoteSwiftPackageReference "SwiftEntryKit" */,
7AE32D6C27F06D2D004EF6E0 /* XCRemoteSwiftPackageReference "SwiftDate" */,
7A1D80DE27F1F275007BD64F /* XCRemoteSwiftPackageReference "DifferenceKit" */,
7A1D80E427F20FCB007BD64F /* XCRemoteSwiftPackageReference "DifferenceKit" */,
);
productRefGroup = 7A49F4A027D4061900AEAAE0 /* Products */;
projectDirPath = "";
@ -1199,6 +1207,22 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
7A1D80DE27F1F275007BD64F /* XCRemoteSwiftPackageReference "DifferenceKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ra1028/DifferenceKit";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
7A1D80E427F20FCB007BD64F /* XCRemoteSwiftPackageReference "DifferenceKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ra1028/DifferenceKit";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
7A28283427E74C110049BDBF /* XCRemoteSwiftPackageReference "SwiftEntryKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/huri000/SwiftEntryKit";
@ -1226,6 +1250,16 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
7A1D80DF27F1F275007BD64F /* DifferenceKit */ = {
isa = XCSwiftPackageProductDependency;
package = 7A1D80DE27F1F275007BD64F /* XCRemoteSwiftPackageReference "DifferenceKit" */;
productName = DifferenceKit;
};
7A1D80E527F20FCB007BD64F /* DifferenceKit */ = {
isa = XCSwiftPackageProductDependency;
package = 7A1D80DE27F1F275007BD64F /* XCRemoteSwiftPackageReference "DifferenceKit" */;
productName = DifferenceKit;
};
7A28283527E74C110049BDBF /* SwiftEntryKit */ = {
isa = XCSwiftPackageProductDependency;
package = 7A28283427E74C110049BDBF /* XCRemoteSwiftPackageReference "SwiftEntryKit" */;

View File

@ -1,5 +1,14 @@
{
"pins" : [
{
"identity" : "differencekit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ra1028/DifferenceKit",
"state" : {
"revision" : "62745d7780deef4a023a792a1f8f763ec7bf9705",
"version" : "1.2.0"
}
},
{
"identity" : "pkhud",
"kind" : "remoteSourceControl",

View File

@ -16,6 +16,27 @@
</dict>
<key>AutoCatCore.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>DifferenceKit (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>6</integer>
</dict>
<key>DifferenceKit (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>7</integer>
</dict>
<key>DifferenceKit (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>1</integer>
</dict>
@ -24,21 +45,21 @@
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>3</integer>
<integer>4</integer>
</dict>
<key>SwiftDate (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>4</integer>
<integer>5</integer>
</dict>
<key>SwiftDate (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>2</integer>
<integer>3</integer>
</dict>
</dict>
</dict>

View File

@ -10,9 +10,60 @@ import AutoCatCore
class VehicleCell: UITableViewCell {
private let numberLabel = UILabel()
private let nameLabel = UILabel()
.disableTranslatesAutoresizingMaskIntoConstraints()
private lazy var nameStackView: UIStackView = {
let stack = UIStackView(arrangedSubviews: [nameLabel])
return stack
}()
private let plateView: PlateView = {
let view = PlateView(frame: .zero)
view.fontSize = 48
view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return view
}()
private let addedDateLabel: UILabel = {
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .footnote)
label.textColor = .tertiaryLabel
return label
}()
private let updatedDateLabel: UILabel = {
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .footnote)
label.textColor = .secondaryLabel
return label
}()
private lazy var dateStackView: UIStackView = {
let stack = UIStackView(arrangedSubviews: [updatedDateLabel, addedDateLabel])
stack.axis = .vertical
stack.spacing = 8
stack.alignment = .trailing
return stack
}()
private lazy var numberStackView: UIStackView = {
let stack = UIStackView(arrangedSubviews: [plateView, dateStackView])
stack.spacing = 8
stack.alignment = .bottom
return stack
}()
private lazy var mainStackView: UIStackView = {
let stack = UIStackView(arrangedSubviews: [nameStackView, numberStackView])
stack.axis = .vertical
stack.spacing = 8
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
let formatter = DateFormatter()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setup()
@ -23,15 +74,40 @@ class VehicleCell: UITableViewCell {
}
func setup() {
contentView.addSubview(numberLabel)
numberLabel.pin(to: contentView, insets: .init(all: 8))
contentView.addSubview(mainStackView)
mainStackView.pin(to: contentView, insets: .init(vertical: 8, horizontal: 16))
formatter.dateStyle = .short
formatter.timeStyle = .short
}
}
extension VehicleCell: ConfigurableCell {
func configure(with item: CDVehicle) {
func configure(with vehicle: CDVehicle) {
guard let number = vehicle.number else {
return
}
plateView.number = PlateNumber(number)
nameLabel.text = vehicle.brand?.name?.original ?? "<Unknown>"
if vehicle.unrecognized {
plateView.foreground = .systemRed
} else if vehicle.outdated {
plateView.foreground = .systemGray3
} else {
plateView.foreground = nil
}
if vehicle.updatedDate - vehicle.addedDate > 60 {
addedDateLabel.text = formatter.string(from: Date(timeIntervalSince1970: vehicle.addedDate))
updatedDateLabel.text = formatter.string(from: Date(timeIntervalSince1970: vehicle.updatedDate))
addedDateLabel.isHidden = false
} else {
addedDateLabel.isHidden = true
updatedDateLabel.text = formatter.string(from: Date(timeIntervalSince1970: vehicle.updatedDate))
}
}
}

View File

@ -8,6 +8,7 @@
import UIKit
import CoreData
import AutoCatCore
import DifferenceKit
protocol ConfigurableCell {
@ -18,28 +19,29 @@ protocol ConfigurableCell {
typealias ConfigurableTableViewCell = UITableViewCell & ConfigurableCell
class CoreDataSource<Item: NSManagedObject, Cell: ConfigurableTableViewCell>: NSObject, NSFetchedResultsControllerDelegate where Cell.Item == Item {
class CoreDataSource<Item: NSManagedObject & Dated & Differentiable, Cell: ConfigurableTableViewCell>
: NSObject, NSFetchedResultsControllerDelegate, UITableViewDataSource where Cell.Item == Item {
private let cellIdentifier: String
private let tableView: UITableView
private let dataSource: UITableViewDiffableDataSource<DateSection<Item>, Item>
//private let dataSource: UITableViewDiffableDataSource<DateSection<Item>, Item>
private let fetchedResults: NSFetchedResultsController<Item>
private var sections: [DateSection<Item>] = []
init(tableView: UITableView, context: NSManagedObjectContext) {
let cellIdentifier = String(describing: Cell.self)
init(tableView: UITableView, context: NSManagedObjectContext, cellIdentifier: String = String(describing: Cell.self)) {
self.tableView = tableView
self.cellIdentifier = cellIdentifier
self.dataSource = UITableViewDiffableDataSource(tableView: tableView) { tv, ip, item in
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier,
for: ip) as? Cell
cell?.configure(with: item)
return cell
}
// self.dataSource = UITableViewDiffableDataSource(tableView: tableView) { tv, ip, item in
// let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier,
// for: ip) as? Cell
// cell?.configure(with: item)
// return cell
// }
if let fetchRequest = Item.fetchRequest() as? NSFetchRequest<Item> {
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "number", ascending: true)]
let fr = NSFetchRequest<Item>(entityName: "Vehicle")
fr.sortDescriptors = []
self.fetchedResults = NSFetchedResultsController(fetchRequest: fr,
fetchRequest.sortDescriptors = []
self.fetchedResults = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil)
@ -49,16 +51,74 @@ class CoreDataSource<Item: NSManagedObject, Cell: ConfigurableTableViewCell>: NS
super.init()
self.tableView.dataSource = self.dataSource
self.tableView.dataSource = self //self.dataSource
self.tableView.reloadData()
self.fetchedResults.delegate = self
try? self.fetchedResults.performFetch()
reload()
}
func reload() {
let items: [Item] = fetchedResults.fetchedObjects ?? []
DispatchQueue.global().async {
let newSections = items.groupedByDate()
let changeset = StagedChangeset(source: self.sections, target: newSections)
DispatchQueue.main.async {
print("reloading tableView")
self.tableView.reload(using: changeset, with: .fade) { newSects in
print("updating sections")
self.sections = newSects
}
}
}
}
// MARK: - NSFetchedResultsControllerDelegate
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
// func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
//
// let snapshotBridged = snapshot as NSDiffableDataSourceSnapshot<DateSection<Item>, Item>
// dataSource.apply(snapshotBridged)
// }
let snapshotBridged = snapshot as NSDiffableDataSourceSnapshot<DateSection<Item>, Item>
dataSource.apply(snapshotBridged)
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
reload()
}
// MARK: - UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int {
return self.sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sections[section].elements.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? Cell else {
return UITableViewCell()
}
let item = self.sections[indexPath.section].elements[indexPath.row]
cell.configure(with: item)
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.sections[section].header
}
}

View File

@ -1,13 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21A559" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21E230" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<entity name="VBrand" representedClassName=".CDVBrand" syncable="YES" codeGenerationType="class">
<attribute name="logo" optional="YES" attributeType="String"/>
<relationship name="name" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="VName" inverseName="brand" inverseEntity="VName"/>
<relationship name="vehicle" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Vehicle" inverseName="brand" inverseEntity="Vehicle"/>
</entity>
<entity name="Vehicle" representedClassName=".CDVehicle" syncable="YES" codeGenerationType="class">
<attribute name="addedDate" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="currentNumber" optional="YES" attributeType="String"/>
<attribute name="number" optional="YES" attributeType="String"/>
<attribute name="updatedDate" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="brand" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="VBrand" inverseName="vehicle" inverseEntity="VBrand"/>
</entity>
<entity name="VName" representedClassName=".CDVName" syncable="YES" codeGenerationType="class">
@ -17,7 +19,7 @@
</entity>
<elements>
<element name="VBrand" positionX="118.7528686523438" positionY="-209.1094360351562" width="128" height="74"/>
<element name="Vehicle" positionX="-145.9427490234375" positionY="-363.4805297851562" width="128" height="74"/>
<element name="Vehicle" positionX="-145.9427490234375" positionY="-363.4805297851562" width="128" height="104"/>
<element name="VName" positionX="-54" positionY="9" width="128" height="74"/>
</elements>
</model>

View File

@ -7,12 +7,18 @@
import Foundation
import SwiftDate
import DifferenceKit
public class DateSection<T: Hashable> {
public protocol Dated {
var addedAt: Date { get }
var updatedAt: Date { get }
}
public class DateSection<T: Hashable & Differentiable>: DifferentiableSection {
private var timestamp: Double = 0
private var header: String
private var elements: [T]
public private(set) var header: String
public private(set) var elements: [T]
public init(timestamp: Double, items: [T]) {
let formatter = DateFormatter()
@ -45,6 +51,26 @@ public class DateSection<T: Hashable> {
self.timestamp = timestamp
self.elements = items
}
public func append(_ element: T) {
self.elements.append(element)
}
// MARK: - DifferentiableSection
public required init<C>(source: DateSection<T>, elements: C) where C : Collection, C.Element == T {
self.timestamp = source.timestamp
self.header = source.header
self.elements = Array(elements)
}
public var differenceIdentifier: String {
return header
}
public func isContentEqual(to source: DateSection<T>) -> Bool {
return differenceIdentifier == source.differenceIdentifier
}
}
extension DateSection: Hashable {
@ -58,3 +84,31 @@ extension DateSection: Hashable {
hasher.combine(self.elements)
}
}
extension RandomAccessCollection where Element: Dated & Hashable & Differentiable {
public func groupedByDate() -> [DateSection<Element>] {
let now = Date()
let monthStart = now.dateAtStartOf(.month)
var sectionsIndices: [TimeInterval: Int] = [:]
var sectionsArray: [DateSection<Element>] = []
for item in self {
let date = item.updatedAt
let dateInRegion = DateInRegion(date, region: Region.current)
var key = dateInRegion.dateAtStartOf(.day).timeIntervalSince1970
if date.isBeforeDate(monthStart, orEqual: false, granularity: .day) {
key = dateInRegion.dateAtStartOf(.month).timeIntervalSince1970
}
if let index = sectionsIndices[key] {
sectionsArray[index].append(item)
} else {
sectionsArray.append(DateSection<Element>(timestamp: key, items: [item]))
sectionsIndices[key] = sectionsArray.count - 1
}
}
return sectionsArray
}
}

View File

@ -1,23 +1,35 @@
import Foundation
import CoreData
import DifferenceKit
public struct Vehicle: Decodable {
let number: String
let currentNumber: String?
let brand: VBrand?
public let number: String
public let currentNumber: String?
public let brand: VBrand?
public let addedDate: TimeInterval
public let updatedDate: TimeInterval
}
// TODO: Remove code duplication
public var unrecognized: Bool {
return self.brand == nil
extension CDVehicle: Dated {
public var updatedAt: Date {
Date(timeIntervalSince1970: updatedDate)
}
public var outdated: Bool {
if let current = self.currentNumber {
return current != self.number
} else {
return false
}
public var addedAt: Date {
Date(timeIntervalSince1970: addedDate)
}
}
extension CDVehicle: Differentiable {
public var differenceIdentifier: String { number! }
public func isContentEqual(to source: CDVehicle) -> Bool {
return number == source.number &&
addedDate == source.addedDate &&
updatedDate == source.updatedDate
}
}
@ -28,6 +40,8 @@ extension CDVehicle {
self.init(context: context)
self.number = vehicle.number
self.currentNumber = vehicle.currentNumber
self.addedDate = vehicle.addedDate/1000
self.updatedDate = vehicle.updatedDate/1000
if let vbrand = vehicle.brand {
self.brand = CDVBrand(vbrand: vbrand, context: context)

View File

@ -9,14 +9,21 @@ protocol StorageServiceProtocol {
public class StorageService: StorageServiceProtocol {
private static var instance: StorageService?
private let container: NSPersistentCloudKitContainer
public static var shared: StorageService {
get async throws {
print("!!!!!!!!!!!!!!!!!!!!!!!!! StorageService init")
let service = StorageService()
try await service.loadPersistentStores()
return service
if let instance = StorageService.instance {
return instance
} else {
print("!!!!!!!!!!!!!!!!!!!!!!!!! StorageService init")
let service = StorageService()
try await service.loadPersistentStores()
StorageService.instance = service
return service
}
}
}