diff --git a/AutoCat2.xcodeproj/project.pbxproj b/AutoCat2.xcodeproj/project.pbxproj
index d3612e7..e856785 100644
--- a/AutoCat2.xcodeproj/project.pbxproj
+++ b/AutoCat2.xcodeproj/project.pbxproj
@@ -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" */;
diff --git a/AutoCat2.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AutoCat2.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index b4b79ad..6a81f52 100644
--- a/AutoCat2.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/AutoCat2.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -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",
diff --git a/AutoCat2.xcodeproj/xcuserdata/selim.xcuserdatad/xcschemes/xcschememanagement.plist b/AutoCat2.xcodeproj/xcuserdata/selim.xcuserdatad/xcschemes/xcschememanagement.plist
index 154e6de..8545f97 100644
--- a/AutoCat2.xcodeproj/xcuserdata/selim.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/AutoCat2.xcodeproj/xcuserdata/selim.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -16,6 +16,27 @@
AutoCatCore.xcscheme_^#shared#^_
+ orderHint
+ 2
+
+ DifferenceKit (Playground) 1.xcscheme
+
+ isShown
+
+ orderHint
+ 6
+
+ DifferenceKit (Playground) 2.xcscheme
+
+ isShown
+
+ orderHint
+ 7
+
+ DifferenceKit (Playground).xcscheme
+
+ isShown
+
orderHint
1
@@ -24,21 +45,21 @@
isShown
orderHint
- 3
+ 4
SwiftDate (Playground) 2.xcscheme
isShown
orderHint
- 4
+ 5
SwiftDate (Playground).xcscheme
isShown
orderHint
- 2
+ 3
diff --git a/AutoCat2/Cells/VehicleCell.swift b/AutoCat2/Cells/VehicleCell.swift
index 2406d70..bce749f 100644
--- a/AutoCat2/Cells/VehicleCell.swift
+++ b/AutoCat2/Cells/VehicleCell.swift
@@ -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 ?? ""
+
+ 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))
+ }
}
}
diff --git a/AutoCat2/Components/TableView/CoreDataSource.swift b/AutoCat2/Components/TableView/CoreDataSource.swift
index cfb3538..a75aeda 100644
--- a/AutoCat2/Components/TableView/CoreDataSource.swift
+++ b/AutoCat2/Components/TableView/CoreDataSource.swift
@@ -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: NSObject, NSFetchedResultsControllerDelegate where Cell.Item == Item {
+class CoreDataSource
+ : NSObject, NSFetchedResultsControllerDelegate, UITableViewDataSource where Cell.Item == Item {
+ private let cellIdentifier: String
private let tableView: UITableView
- private let dataSource: UITableViewDiffableDataSource, Item>
+ //private let dataSource: UITableViewDiffableDataSource, Item>
private let fetchedResults: NSFetchedResultsController-
+ private var sections: [DateSection
- ] = []
- 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
- {
- fetchRequest.sortDescriptors = [NSSortDescriptor(key: "number", ascending: true)]
- let fr = NSFetchRequest
- (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: 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, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
+// func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
+//
+// let snapshotBridged = snapshot as NSDiffableDataSourceSnapshot, Item>
+// dataSource.apply(snapshotBridged)
+// }
+
+ func controllerWillChangeContent(_ controller: NSFetchedResultsController) {
+ }
+
+ func controllerDidChangeContent(_ controller: NSFetchedResultsController) {
+ }
+
+ func controller(_ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
+ }
+
+ func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
- let snapshotBridged = snapshot as NSDiffableDataSourceSnapshot, Item>
- dataSource.apply(snapshotBridged)
+ 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
}
}
diff --git a/AutoCatCore/AutoCat2.xcdatamodeld/Shared.xcdatamodel/contents b/AutoCatCore/AutoCat2.xcdatamodeld/Shared.xcdatamodel/contents
index 480e6a4..c70a61c 100644
--- a/AutoCatCore/AutoCat2.xcdatamodeld/Shared.xcdatamodel/contents
+++ b/AutoCatCore/AutoCat2.xcdatamodeld/Shared.xcdatamodel/contents
@@ -1,13 +1,15 @@
-
+
+
+
@@ -17,7 +19,7 @@
-
+
\ No newline at end of file
diff --git a/AutoCatCore/DataSource/DateSection.swift b/AutoCatCore/DataSource/DateSection.swift
index 3becccf..46a11e4 100644
--- a/AutoCatCore/DataSource/DateSection.swift
+++ b/AutoCatCore/DataSource/DateSection.swift
@@ -7,12 +7,18 @@
import Foundation
import SwiftDate
+import DifferenceKit
-public class DateSection {
+public protocol Dated {
+ var addedAt: Date { get }
+ var updatedAt: Date { get }
+}
+public class DateSection: 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 {
self.timestamp = timestamp
self.elements = items
}
+
+ public func append(_ element: T) {
+ self.elements.append(element)
+ }
+
+ // MARK: - DifferentiableSection
+
+ public required init(source: DateSection, 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) -> 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] {
+ let now = Date()
+ let monthStart = now.dateAtStartOf(.month)
+ var sectionsIndices: [TimeInterval: Int] = [:]
+ var sectionsArray: [DateSection] = []
+ 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(timestamp: key, items: [item]))
+ sectionsIndices[key] = sectionsArray.count - 1
+ }
+ }
+
+ return sectionsArray
+ }
+}
diff --git a/AutoCatCore/Models/Vehicle.swift b/AutoCatCore/Models/Vehicle.swift
index 0b9af43..4a56331 100644
--- a/AutoCatCore/Models/Vehicle.swift
+++ b/AutoCatCore/Models/Vehicle.swift
@@ -1,23 +1,35 @@
import Foundation
import CoreData
+import DifferenceKit
public struct Vehicle: Decodable {
- let number: String
- let currentNumber: String?
- let brand: VBrand?
-
- // TODO: Remove code duplication
- public var unrecognized: Bool {
- return self.brand == nil
+ public let number: String
+ public let currentNumber: String?
+ public let brand: VBrand?
+ public let addedDate: TimeInterval
+ public let updatedDate: TimeInterval
+}
+
+extension CDVehicle: Dated {
+
+ public var updatedAt: Date {
+ Date(timeIntervalSince1970: updatedDate)
}
+
+ public var addedAt: Date {
+ Date(timeIntervalSince1970: addedDate)
+ }
+}
+
+extension CDVehicle: Differentiable {
- public var outdated: Bool {
- if let current = self.currentNumber {
- return current != self.number
- } else {
- return false
- }
+ 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)
diff --git a/AutoCatCore/Services/StorageService.swift b/AutoCatCore/Services/StorageService.swift
index 175f250..204b0ce 100644
--- a/AutoCatCore/Services/StorageService.swift
+++ b/AutoCatCore/Services/StorageService.swift
@@ -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
+ }
}
}