From 1ab4de19e0f4d4142f0012de5b06c5ce897bf364 Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Mon, 5 Oct 2020 01:53:58 +0300 Subject: [PATCH] Refactoring of displaying realm results in a sectioned list --- AutoCat.xcodeproj/project.pbxproj | 8 + .../xcshareddata/swiftpm/Package.resolved | 20 +- .../xcschemes/xcschememanagement.plist | 24 ++- AutoCat/AppDelegate.swift | 22 +- AutoCat/Cells/AudioRecordCell.swift | 2 +- AutoCat/Cells/ConfigurableCell.swift | 6 + AutoCat/Cells/VehicleCell.swift | 4 +- AutoCat/Controllers/CheckController.swift | 196 ++++++++---------- AutoCat/Controllers/RecordsController.swift | 30 +-- AutoCat/Extensions/Dated.swift | 7 +- AutoCat/Models/DateSection.swift | 2 +- AutoCat/Models/Vehicle.swift | 12 +- AutoCat/Utils/RxRealmDataSource.swift | 89 ++++++++ 13 files changed, 261 insertions(+), 161 deletions(-) create mode 100644 AutoCat/Cells/ConfigurableCell.swift create mode 100644 AutoCat/Utils/RxRealmDataSource.swift diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index bf4e703..073e906 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ 7A96AE2F246B2BCD00297C33 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A96AE2E246B2BCD00297C33 /* WebKit.framework */; }; 7A96AE31246B2FE400297C33 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A96AE30246B2FE400297C33 /* Constants.swift */; }; 7A96AE33246C095700297C33 /* Base64FS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A96AE32246C095700297C33 /* Base64FS.swift */; }; + 7A9FEEC82529AB23001CA50E /* RxRealmDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */; }; 7AAE6AD324CDDF950023860B /* VehicleEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAE6AD224CDDF950023860B /* VehicleEvent.swift */; }; 7AB562BA249C9E9B00473D53 /* Region.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB562B9249C9E9B00473D53 /* Region.swift */; }; 7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8B2435C38700258F61 /* CustomTextField.swift */; }; @@ -102,6 +103,7 @@ 7AE24C5F251F1B4E00758E39 /* Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE24C5E251F1B4E00758E39 /* Buttons.swift */; }; 7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */; }; 7AE26A3524F31B0700625033 /* EventsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE26A3424F31B0700625033 /* EventsController.swift */; }; + 7AEFC3BE2529D3CC00BADFB2 /* ConfigurableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */; }; 7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFE727240455E200910EB7 /* SettingsController.swift */; }; 7AF58D2F24029C5200CE01A0 /* MagazineLayout in Frameworks */ = {isa = PBXBuildFile; productRef = 7AF58D2E24029C5200CE01A0 /* MagazineLayout */; }; 7AF58D3124029E1000CE01A0 /* VehicleHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF58D3024029E1000CE01A0 /* VehicleHeaderCell.swift */; }; @@ -184,6 +186,7 @@ 7A96AE2E246B2BCD00297C33 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; }; 7A96AE30246B2FE400297C33 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 7A96AE32246C095700297C33 /* Base64FS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base64FS.swift; sourceTree = ""; }; + 7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxRealmDataSource.swift; sourceTree = ""; }; 7AAE6AD224CDDF950023860B /* VehicleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEvent.swift; sourceTree = ""; }; 7AB562B9249C9E9B00473D53 /* Region.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Region.swift; sourceTree = ""; }; 7AB67E8B2435C38700258F61 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = ""; }; @@ -198,6 +201,7 @@ 7AE24C5E251F1B4E00758E39 /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = ""; }; 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExt.swift; sourceTree = ""; }; 7AE26A3424F31B0700625033 /* EventsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsController.swift; sourceTree = ""; }; + 7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurableCell.swift; sourceTree = ""; }; 7AEFE727240455E200910EB7 /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = ""; }; 7AF58D3024029E1000CE01A0 /* VehicleHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleHeaderCell.swift; sourceTree = ""; }; 7AF58D57240309CA00CE01A0 /* VehicleTextParamCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleTextParamCell.swift; sourceTree = ""; }; @@ -311,6 +315,7 @@ 7A27ADF6249FEF690035F39E /* Recorder.swift */, 7A1090E924A3A26300B4F0B2 /* AudioPlayer.swift */, 7A000AA124C2EEDE001F5B00 /* Location.swift */, + 7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */, ); path = Utils; sourceTree = ""; @@ -383,6 +388,7 @@ 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */, 7A1090E724A394F100B4F0B2 /* AudioRecordCell.swift */, 7A813DC22508EE4F00CC93B9 /* EventCell.swift */, + 7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */, ); path = Cells; sourceTree = ""; @@ -550,6 +556,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7AEFC3BE2529D3CC00BADFB2 /* ConfigurableCell.swift in Sources */, 7A96AE33246C095700297C33 /* Base64FS.swift in Sources */, 7A530B802401803A00CBFE6E /* Vehicle.swift in Sources */, 7A96AE31246B2FE400297C33 /* Constants.swift in Sources */, @@ -610,6 +617,7 @@ 7A813DCB250B5DC900CC93B9 /* LocationPickerController.swift in Sources */, 7A11474423FF06CA00B424AF /* Api.swift in Sources */, 7A488C3D24A74B990054D0B2 /* RxCollectionViewRealmDataSource.swift in Sources */, + 7A9FEEC82529AB23001CA50E /* RxRealmDataSource.swift in Sources */, 7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */, 7A21112A24FC3D7E003BBF6F /* AudioEngine.swift in Sources */, 7A8A220B248D67B60073DFD9 /* VehicleReportImage.swift in Sources */, diff --git a/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b4870ea..8bb26d0 100644 --- a/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/xmartlabs/Eureka", "state": { "branch": null, - "revision": "9fe153d2be663621d4f53196bf1b7effb83d7017", - "version": "5.3.0" + "revision": "ae025915335f02e7277ed5446b4de002bab05866", + "version": "5.3.1" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/onevcat/Kingfisher", "state": { "branch": null, - "revision": "22b795dd57766fa0e86773d8733efb31dc0f31fb", - "version": "5.15.1" + "revision": "2a6d1135af3915547c4b08c3b154a05e6f1075a3", + "version": "5.15.5" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/airbnb/MagazineLayout", "state": { "branch": null, - "revision": "4a5eff2203ad8d8c7e14ea1b283b64f9320752a9", - "version": "1.6.2" + "revision": "6f88742c282de208e48cb738a7a14b7dc2651701", + "version": "1.6.3" } }, { @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/realm/realm-cocoa", "state": { "branch": null, - "revision": "f64ac045d8cb171d8e317d9b854df7215aed7466", - "version": "5.4.2" + "revision": "2dc2d259095051b997b76a07e859822661105303", + "version": "5.4.7" } }, { @@ -51,8 +51,8 @@ "repositoryURL": "https://github.com/realm/realm-core", "state": { "branch": null, - "revision": "e051fc73c56830bf3ab0b8a82f7a613968cec6c6", - "version": "6.0.26" + "revision": "2df510904ad04287926b287b4e89b786de2808c8", + "version": "6.1.3" } }, { diff --git a/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcschemes/xcschememanagement.plist b/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcschemes/xcschememanagement.plist index 825a033..7cd6e44 100644 --- a/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,28 +7,28 @@ AutoCat.xcscheme_^#shared#^_ orderHint - 1 + 0 Eureka (Playground) 1.xcscheme isShown orderHint - 5 + 3 Eureka (Playground) 2.xcscheme isShown orderHint - 6 + 4 Eureka (Playground).xcscheme isShown orderHint - 4 + 2 GettingStarted (Playground) 1.xcscheme @@ -77,21 +77,21 @@ isShown orderHint - 2 + 6 Rx (Playground) 2.xcscheme isShown orderHint - 3 + 7 Rx (Playground).xcscheme isShown orderHint - 0 + 5 SwiftDate (Playground) 1.xcscheme @@ -133,7 +133,15 @@ isShown orderHint - 7 + 1 + + + SuppressBuildableAutocreation + + 7A1146FC23FDE7E500B424AF + + primary + diff --git a/AutoCat/AppDelegate.swift b/AutoCat/AppDelegate.swift index 780df1c..913574d 100644 --- a/AutoCat/AppDelegate.swift +++ b/AutoCat/AppDelegate.swift @@ -24,7 +24,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let config = Realm.Configuration( - schemaVersion: 18, + schemaVersion: 19, migrationBlock: { migration, oldSchemaVersion in if oldSchemaVersion <= 3 { var numbers: [String] = [] @@ -44,6 +44,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate { new!["isRightWheel"] = RealmOptional(old!["isRightWheel"] as? Bool) } } + + if oldSchemaVersion <= 18 { + migration.enumerateObjects(ofType: "Vehicle") { old, new in + let addedDate = old!["addedDate"] as! TimeInterval + let events = old!.dynamicList("events") + new!["addedDate"] = addedDate/1000 + + let lastEvent = events.max { first, second in + let firstDate = first["date"] as! TimeInterval + let secondDate = second["date"] as! TimeInterval + return firstDate < secondDate + } + + if let lastEvent = lastEvent { + new!["updatedDate"] = max(addedDate/1000, lastEvent["date"] as! TimeInterval) + } else { + new!["updatedDate"] = addedDate/1000 + } + } + } }) Realm.Configuration.defaultConfiguration = config diff --git a/AutoCat/Cells/AudioRecordCell.swift b/AutoCat/Cells/AudioRecordCell.swift index 8c233ee..b3f71d9 100644 --- a/AutoCat/Cells/AudioRecordCell.swift +++ b/AutoCat/Cells/AudioRecordCell.swift @@ -1,7 +1,7 @@ import UIKit import RxSwift -class AudioRecordCell: UITableViewCell { +class AudioRecordCell: UITableViewCell, ConfigurableCell { @IBOutlet weak var playButton: UIButton! @IBOutlet weak var duration: UILabel! diff --git a/AutoCat/Cells/ConfigurableCell.swift b/AutoCat/Cells/ConfigurableCell.swift new file mode 100644 index 0000000..d2eb25a --- /dev/null +++ b/AutoCat/Cells/ConfigurableCell.swift @@ -0,0 +1,6 @@ +import UIKit + +protocol ConfigurableCell { + associatedtype Item + func configure(with item: Item) +} diff --git a/AutoCat/Cells/VehicleCell.swift b/AutoCat/Cells/VehicleCell.swift index eef8b5d..8c052ed 100644 --- a/AutoCat/Cells/VehicleCell.swift +++ b/AutoCat/Cells/VehicleCell.swift @@ -1,6 +1,6 @@ import UIKit -class VehicleCell: UITableViewCell { +class VehicleCell: UITableViewCell, ConfigurableCell { @IBOutlet weak var name: UILabel! @IBOutlet weak var plate: PlateView! @@ -25,7 +25,7 @@ class VehicleCell: UITableViewCell { } else { self.plate.foreground = nil } - self.date.text = formatter.string(from: Date(timeIntervalSince1970: vehicle.addedDate/1000)) + self.date.text = formatter.string(from: Date(timeIntervalSince1970: vehicle.addedDate)) } } diff --git a/AutoCat/Controllers/CheckController.swift b/AutoCat/Controllers/CheckController.swift index 6cb6188..974a61b 100644 --- a/AutoCat/Controllers/CheckController.swift +++ b/AutoCat/Controllers/CheckController.swift @@ -3,7 +3,6 @@ import RealmSwift import RxSwift import SwiftDate import RxRealm -import RxDataSources enum EventAction { case doNotSend @@ -18,6 +17,7 @@ class CheckController: UIViewController, UITableViewDelegate, UITextFieldDelegat @IBOutlet weak var history: UITableView! let bag = DisposeBag() + var historyDataSource: RealmSectionedDataSource! // MARK: - Lifecycle @@ -33,32 +33,11 @@ class CheckController: UIViewController, UITableViewDelegate, UITextFieldDelegat self.number.inputView = keyboard self.check.isEnabled = false - let ds = RxTableViewSectionedAnimatedDataSource>(configureCell: { dataSource, tableView, indexPath, item in - if let cell = tableView.dequeueReusableCell(withIdentifier: "VehicleCell", for: indexPath) as? VehicleCell { - cell.configure(with: item) - return cell - } else { - return UITableViewCell() - } - }, canEditRowAtIndexPath: { _, _ in true }) - - ds.titleForHeaderInSection = { dataSourse, index in - return dataSourse.sectionModels[index].header - } - - self.history.rx.modelSelected(Vehicle.self) - .subscribe(onNext: self.updateDetailController(with:)) - .disposed(by: self.bag) - DispatchQueue.main.async { - Observable.collection(from: realm.objects(Vehicle.self) - .sorted(byKeyPath: "addedDate", ascending: false)) - .map { $0.groupedByDate() } - .bind(to: self.history.rx.items(dataSource: ds)) - .disposed(by: self.bag) + self.historyDataSource = RealmSectionedDataSource(table: self.history, data: realm.objects(Vehicle.self).sorted(byKeyPath: "updatedDate", ascending: false)) } - self.history.rx.setDelegate(self).disposed(by: self.bag) + self.history.delegate = self } override func viewWillAppear(_ animated: Bool) { @@ -94,7 +73,7 @@ class CheckController: UIViewController, UITableViewDelegate, UITextFieldDelegat if let event = event { action = .sendSpecific(event) } - self.check(number: number, action: action) + self.check(number: number, action: action).subscribe().disposed(by: self.bag) break case .addVoiceRecord: self.tabBarController?.selectedIndex = 1 @@ -116,83 +95,19 @@ class CheckController: UIViewController, UITableViewDelegate, UITextFieldDelegat guard let number = self.number.text else { return } let numberNormalized = number.filter { !$0.isWhitespace }.uppercased() - self.check(number: numberNormalized, action: .receiveAndSend) - } - - func check(number: String, action: EventAction) { self.number.resignFirstResponder() self.number.text = nil self.check.isEnabled = false IHProgressHUD.show() - Api.checkVehicle(by: number) - .observeOn(MainScheduler.instance) - .subscribe(onSuccess: { vehicle in - self.onReceivedVehicle(vehicle, action: action) - }, onError: { err in - if let realm = try? Realm() { - let vehicle = Vehicle(number) - try? realm.write { - realm.add(vehicle, update: .all) - } - - var eventSingle = self.getEvent() - if case .sendSpecific(let event) = action { - eventSingle = Single.just(event) - } - eventSingle - .flatMap { event in event.findAddress().map{ [event] }.catchErrorJustReturn([event]) } - .catchErrorJustReturn([]) - .subscribe(onSuccess: { events in - try? realm.write { - vehicle.events.append(objectsIn: events) - } - }) - .disposed(by: self.bag) - } - IHProgressHUD.showError(withStatus: err.localizedDescription) - print(err.localizedDescription) - }).disposed(by: self.bag) - } - - func save(vehicle: Vehicle) { - if let realm = try? Realm() { - try? realm.write { - realm.add(vehicle, update: .all) - } - } - } - - func getEvent() -> Single { - if let event = LocationManager.lastEvent, (Date().timeIntervalSince1970 - event.date) < 30 { - print("Using last event") - return Single.just(event) - } else { - print("requesting new event") - return LocationManager.requestCurrentLocation() - } - } - - func onReceivedVehicle(_ vehicle: Vehicle, action: EventAction = .receiveAndSend) { - self.save(vehicle: vehicle) - if case .doNotSend = action { - // Just do nothing - } else { - var eventSingle = self.getEvent() - if case .sendSpecific(let event) = action { - eventSingle = Single.just(event) - } - eventSingle - .flatMap { event in event.findAddress().map{ event }.catchErrorJustReturn(event) } - .flatMap { - Api.add(event: $0, to: vehicle.getNumber()) - } - .subscribe(onSuccess: self.save(vehicle:), onError: { print("Error adding event: \($0)") }) - .disposed(by: self.bag) + self.check(number: numberNormalized, action: .receiveAndSend).subscribe { vehicle in + self.updateDetailController(with: vehicle) + IHProgressHUD.dismiss() + } onError: { error in + IHProgressHUD.show(error: error) } - - self.updateDetailController(with: vehicle) - IHProgressHUD.dismiss() + .disposed(by: self.bag) + } func updateDetailController(with vehicle: Vehicle) { @@ -239,27 +154,27 @@ class CheckController: UIViewController, UITableViewDelegate, UITextFieldDelegat } } - // MARK: - + // MARK: - UITableViewDelegate func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - guard let vehicle: Vehicle = try? self.history.rx.model(at: indexPath) else { return nil } + let vehicle = self.historyDataSource.item(at: indexPath) let updateAction = UIContextualAction(style: .normal, title: "Update") { action, view, completion in IHProgressHUD.show() var eventAction: EventAction = .doNotSend if vehicle.unrecognized, let savedEvent = vehicle.events.first { - eventAction = .sendSpecific(savedEvent.freeze()) + eventAction = .sendSpecific(savedEvent) } - Api.checkVehicle(by: vehicle.getNumber(), force: true) - .observeOn(MainScheduler.instance) - .subscribe(onSuccess: { vehicle in - self.onReceivedVehicle(vehicle, action: eventAction) - }, onError: { err in - IHProgressHUD.showError(withStatus: err.localizedDescription) - print(err.localizedDescription) - }).disposed(by: self.bag) + self.check(number: vehicle.getNumber(), action: eventAction, force: true).subscribe { vehicle in + self.updateDetailController(with: vehicle) + IHProgressHUD.dismiss() + } onError: { error in + IHProgressHUD.show(error: error) + } + .disposed(by: self.bag) + completion(true) } updateAction.image = UIImage(systemName: "arrow.2.circlepath") @@ -282,4 +197,71 @@ class CheckController: UIViewController, UITableViewDelegate, UITextFieldDelegat configuration.performsFirstActionWithFullSwipe = false return configuration } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let vehicle = self.historyDataSource.item(at: indexPath) + self.updateDetailController(with: vehicle) + } + + // MARK: - Checking number + + func save(vehicle: Vehicle) throws { + let realm = try Realm() + try realm.write { + print("===== Save vehicle: \(vehicle.getNumber())") + realm.add(vehicle, update: .all) + } + } + + func getEvent(for action: EventAction) -> Single { + if case .sendSpecific(let event) = action { + return Single.just(event) + } + + if let event = LocationManager.lastEvent, (Date().timeIntervalSince1970 - event.date) < 30 { + return Single.just(event) + } else { + return LocationManager.requestCurrentLocation() + } + } + + func check(number: String, action: EventAction, force: Bool = false) -> Single { + let checkSingle = Api.checkVehicle(by: number, force: force) + .observeOn(MainScheduler.instance) + .map { (vehicle: Vehicle) -> Vehicle in + try self.save(vehicle: vehicle) + return vehicle + } + .do (onError: { err in + let realm = try Realm() + if realm.object(ofType: Vehicle.self, forPrimaryKey: number) == nil { + let vehicle = Vehicle(number) + try realm.write { realm.add(vehicle, update: .all) } + + self.getEvent(for: action) + .flatMap { event in event.findAddress().map{ [event] }.catchErrorJustReturn([event]) } + .catchErrorJustReturn([]) + .map { events in + try realm.write { vehicle.events.append(objectsIn: events) } + } + .subscribe() + .disposed(by: self.bag) + } + }) + + + if case .doNotSend = action { + return checkSingle + } else { + return checkSingle + .flatMap { _ in self.getEvent(for: action) } + .flatMap { event in event.findAddress().map{ event }.catchErrorJustReturn(event) } + .flatMap { Api.add(event: $0, to: number) } + .observeOn(MainScheduler.instance) + .map { + try self.save(vehicle: $0) + return $0 + } + } + } } diff --git a/AutoCat/Controllers/RecordsController.swift b/AutoCat/Controllers/RecordsController.swift index f2fdf06..d302e6a 100644 --- a/AutoCat/Controllers/RecordsController.swift +++ b/AutoCat/Controllers/RecordsController.swift @@ -3,7 +3,6 @@ import AVFoundation import RealmSwift import RxSwift import RxRealm -import RxDataSources import Intents import CoreSpotlight import MobileCoreServices @@ -18,6 +17,7 @@ class RecordsController: UIViewController, UITableViewDelegate { let bag = DisposeBag() var recordDisposable: Disposable? var audioSessionObserver: NSObjectProtocol? + var recordsDataSource: RealmSectionedDataSource! let validLetters = Constants.pnLettersMap.keys.map(String.init) @@ -31,28 +31,11 @@ class RecordsController: UIViewController, UITableViewDelegate { self.recorder = Recorder() - let ds = RxTableViewSectionedAnimatedDataSource>(configureCell: { dataSource, tableView, indexPath, item in - if let cell = tableView.dequeueReusableCell(withIdentifier: "AudioRecordCell", for: indexPath) as? AudioRecordCell { - cell.configure(with: item.freeze()) - return cell - } else { - return UITableViewCell() - } - }, canEditRowAtIndexPath: { _, _ in true }) - - ds.titleForHeaderInSection = { dataSourse, index in - return dataSourse.sectionModels[index].header - } - DispatchQueue.main.async { - Observable.collection(from: realm.objects(AudioRecord.self) - .sorted(byKeyPath: "addedDate", ascending: false)) - .map { $0.groupedByDate() } - .bind(to: self.tableView.rx.items(dataSource: ds)) - .disposed(by: self.bag) + self.recordsDataSource = RealmSectionedDataSource(table: self.tableView, data: realm.objects(AudioRecord.self) + .sorted(byKeyPath: "addedDate", ascending: false)) } - - self.tableView.rx.setDelegate(self).disposed(by: self.bag) + self.tableView.delegate = self } override func viewDidAppear(_ animated: Bool) { @@ -241,9 +224,10 @@ class RecordsController: UIViewController, UITableViewDelegate { // MARK: - UITableViewDelegate func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - guard let record: AudioRecord = try? self.tableView.rx.model(at: indexPath) else { return nil } guard let cell = tableView.cellForRow(at: indexPath) else { return nil } + let record = self.recordsDataSource.item(at: indexPath) + let check = UIContextualAction(style: .normal, title: "Check") { action, view, completion in if let number = record.number { self.check(number: number, event: record.event) @@ -339,7 +323,7 @@ class RecordsController: UIViewController, UITableViewDelegate { alert.dismiss(animated: true) })) alert.addTextField { tf in - tf.text = record.number ?? record.rawText + tf.text = record.number ?? record.rawText.replacingOccurrences(of: " ", with: "") NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: tf, queue: OperationQueue.main) { _ in done.isEnabled = self.valid(number: tf.text?.uppercased() ?? "") } diff --git a/AutoCat/Extensions/Dated.swift b/AutoCat/Extensions/Dated.swift index 42472b6..7f5827f 100644 --- a/AutoCat/Extensions/Dated.swift +++ b/AutoCat/Extensions/Dated.swift @@ -8,12 +8,7 @@ protocol Dated { extension Vehicle: Dated { var date: Date { - if let lastEventDate = self.events.max(by: { $0.date < $1.date })?.date { - let ts = max(self.addedDate/1000, lastEventDate) - return Date(timeIntervalSince1970: ts) - } else { - return Date(timeIntervalSince1970: self.addedDate/1000) - } + return Date(timeIntervalSince1970: self.updatedDate) } } diff --git a/AutoCat/Models/DateSection.swift b/AutoCat/Models/DateSection.swift index cefadc5..0ba4c7e 100644 --- a/AutoCat/Models/DateSection.swift +++ b/AutoCat/Models/DateSection.swift @@ -2,7 +2,7 @@ import Foundation import RxDataSources import SwiftDate -struct DateSection: AnimatableSectionModelType where T: IdentifiableType, T: Equatable { +struct DateSection: AnimatableSectionModelType, Equatable where T: IdentifiableType, T: Equatable { var timestamp: Double = 0 var header: String diff --git a/AutoCat/Models/Vehicle.swift b/AutoCat/Models/Vehicle.swift index 1f8f7ca..295e036 100644 --- a/AutoCat/Models/Vehicle.swift +++ b/AutoCat/Models/Vehicle.swift @@ -82,6 +82,7 @@ class Vehicle: Object, Decodable, IdentifiableType { var isRightWheel = RealmOptional() @objc dynamic var isJapanese: Bool = false @objc dynamic var addedDate: TimeInterval = 0 + @objc dynamic var updatedDate: TimeInterval = 0 @objc dynamic var addedBy: String = "" let photos = List() let ownershipPeriods = List() @@ -133,7 +134,7 @@ class Vehicle: Object, Decodable, IdentifiableType { self.pts = try container.decodeIfPresent(String.self, forKey: .pts) self.isRightWheel = try container.decode(RealmOptional.self, forKey: .isRightWheel) self.isJapanese = try container.decode(Bool.self, forKey: .isJapanese) - self.addedDate = try container.decode(TimeInterval.self, forKey: .addedDate) + self.addedDate = (try container.decode(TimeInterval.self, forKey: .addedDate))/1000 self.addedBy = try container.decode(String.self, forKey: .addedBy) if let photosArray = try container.decodeIfPresent([VehiclePhoto].self, forKey: .photos) { @@ -148,6 +149,12 @@ class Vehicle: Object, Decodable, IdentifiableType { self.events.append(objectsIn: eventsArray) } + if let lastEventDate = self.events.max(by: { $0.date < $1.date })?.date { + self.updatedDate = max(self.addedDate, lastEventDate) + } else { + self.updatedDate = self.addedDate + } + self.identifier = self.number } @@ -159,7 +166,8 @@ class Vehicle: Object, Decodable, IdentifiableType { init(_ number: String) { self.identifier = number self.number = number - self.addedDate = Date().timeIntervalSince1970*1000 + self.addedDate = Date().timeIntervalSince1970 + self.updatedDate = self.addedDate } func getNumber() -> String { diff --git a/AutoCat/Utils/RxRealmDataSource.swift b/AutoCat/Utils/RxRealmDataSource.swift new file mode 100644 index 0000000..901a42d --- /dev/null +++ b/AutoCat/Utils/RxRealmDataSource.swift @@ -0,0 +1,89 @@ +import UIKit +import RealmSwift +import RxDataSources + +class RealmSectionedDataSource: NSObject, UITableViewDataSource where Item: Object & IdentifiableType & Dated, Cell: UITableViewCell & ConfigurableCell, Cell.Item == Item { + + private var tv: UITableView + private var data: Results + private var notificationToken: NotificationToken? + private var sections: [DateSection] = [] + private var cellIdentifier: String + + init(table: UITableView, data: Results, cellIdentifier: String = String(describing: Cell.self)) { + self.tv = table + self.data = data + self.cellIdentifier = cellIdentifier + super.init() + self.tv.dataSource = self + + self.notificationToken = self.data.observe { changes in + switch changes { + case .initial: + self.sections = self.data.groupedByDate() + self.tv.reloadData() + case .update(_, let deletions, let insertions, let modifications): + print("Deletions: \(deletions.count), Insertions: \(insertions.count), Modifications: \(modifications.count)") + let newSections = self.data.groupedByDate() + let diff = newSections.difference(from: self.sections) + + self.tv.beginUpdates() + let delPaths = deletions.map(self.indexPath) + self.tv.deleteRows(at: delPaths, with: .automatic) + //diff.filter { if case .remove = $0 } + self.sections = self.data.groupedByDate() + self.tv.insertRows(at: insertions.map(self.indexPath), with: .automatic) + self.tv.reloadRows(at: modifications.map(self.indexPath), with: .automatic) + self.tv.endUpdates() + case .error(let err): + print("Realm observer error: \(err)") + } + } + } + + // Here I assume that sections keep order of elements the same as in initial flat collection + // Otherwise it will not work properly + func indexPath(by index: Int) -> IndexPath { + var sectionStartIndex = 0 + for (i, section) in self.sections.enumerated() { + if index < sectionStartIndex + section.items.count { + print("Index \(index) -> (\(i), \(index - sectionStartIndex))") + return IndexPath(row: index - sectionStartIndex, section: i) + } + + sectionStartIndex += section.items.count + } + + return IndexPath(row: 0, section: 0) + } + + // MARK: - UITableViewDataSource + + func numberOfSections(in tableView: UITableView) -> Int { + return self.sections.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.sections[section].items.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: self.cellIdentifier, for: indexPath) as? Cell else { + return UITableViewCell() + } + + let item = self.sections[indexPath.section].items[indexPath.row] + cell.configure(with: item) + return cell + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return self.sections[section].header + } + + // MARK: - Public + + func item(at indexPath: IndexPath) -> Item { + return self.sections[indexPath.section].items[indexPath.row] + } +}