From aa7efbe1b08db5841ff6dc6014214b35a262f7ea Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Mon, 28 Mar 2022 20:10:47 +0300 Subject: [PATCH] Displaying history --- AutoCat2.xcodeproj/project.pbxproj | 34 +++++++ .../xcshareddata/swiftpm/Package.resolved | 9 ++ .../xcschemes/xcschememanagement.plist | 27 +++++- AutoCat2/Cells/VehicleCell.swift | 84 +++++++++++++++- .../Components/TableView/CoreDataSource.swift | 96 +++++++++++++++---- .../Shared.xcdatamodel/contents | 6 +- AutoCatCore/DataSource/DateSection.swift | 60 +++++++++++- AutoCatCore/Models/Vehicle.swift | 40 +++++--- AutoCatCore/Services/StorageService.swift | 15 ++- 9 files changed, 324 insertions(+), 47 deletions(-) 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 + } } }