Refactoring of displaying realm results in a sectioned list
This commit is contained in:
parent
cbfe729440
commit
1ab4de19e0
@ -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 = "<group>"; };
|
||||
7A96AE32246C095700297C33 /* Base64FS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base64FS.swift; sourceTree = "<group>"; };
|
||||
7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxRealmDataSource.swift; sourceTree = "<group>"; };
|
||||
7AAE6AD224CDDF950023860B /* VehicleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEvent.swift; sourceTree = "<group>"; };
|
||||
7AB562B9249C9E9B00473D53 /* Region.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Region.swift; sourceTree = "<group>"; };
|
||||
7AB67E8B2435C38700258F61 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; };
|
||||
@ -198,6 +201,7 @@
|
||||
7AE24C5E251F1B4E00758E39 /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = "<group>"; };
|
||||
7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExt.swift; sourceTree = "<group>"; };
|
||||
7AE26A3424F31B0700625033 /* EventsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsController.swift; sourceTree = "<group>"; };
|
||||
7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurableCell.swift; sourceTree = "<group>"; };
|
||||
7AEFE727240455E200910EB7 /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = "<group>"; };
|
||||
7AF58D3024029E1000CE01A0 /* VehicleHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleHeaderCell.swift; sourceTree = "<group>"; };
|
||||
7AF58D57240309CA00CE01A0 /* VehicleTextParamCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleTextParamCell.swift; sourceTree = "<group>"; };
|
||||
@ -311,6 +315,7 @@
|
||||
7A27ADF6249FEF690035F39E /* Recorder.swift */,
|
||||
7A1090E924A3A26300B4F0B2 /* AudioPlayer.swift */,
|
||||
7A000AA124C2EEDE001F5B00 /* Location.swift */,
|
||||
7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
@ -383,6 +388,7 @@
|
||||
7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */,
|
||||
7A1090E724A394F100B4F0B2 /* AudioRecordCell.swift */,
|
||||
7A813DC22508EE4F00CC93B9 /* EventCell.swift */,
|
||||
7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */,
|
||||
);
|
||||
path = Cells;
|
||||
sourceTree = "<group>";
|
||||
@ -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 */,
|
||||
|
||||
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -7,28 +7,28 @@
|
||||
<key>AutoCat.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>Eureka (Playground) 1.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>5</integer>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>Eureka (Playground) 2.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>6</integer>
|
||||
<integer>4</integer>
|
||||
</dict>
|
||||
<key>Eureka (Playground).xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>4</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>GettingStarted (Playground) 1.xcscheme</key>
|
||||
<dict>
|
||||
@ -77,21 +77,21 @@
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>6</integer>
|
||||
</dict>
|
||||
<key>Rx (Playground) 2.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
<integer>7</integer>
|
||||
</dict>
|
||||
<key>Rx (Playground).xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
<integer>5</integer>
|
||||
</dict>
|
||||
<key>SwiftDate (Playground) 1.xcscheme</key>
|
||||
<dict>
|
||||
@ -133,7 +133,15 @@
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>7</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>7A1146FC23FDE7E500B424AF</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@ -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<Bool>(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
|
||||
|
||||
@ -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!
|
||||
|
||||
6
AutoCat/Cells/ConfigurableCell.swift
Normal file
6
AutoCat/Cells/ConfigurableCell.swift
Normal file
@ -0,0 +1,6 @@
|
||||
import UIKit
|
||||
|
||||
protocol ConfigurableCell {
|
||||
associatedtype Item
|
||||
func configure(with item: Item)
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<Vehicle, VehicleCell>!
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
@ -33,32 +33,11 @@ class CheckController: UIViewController, UITableViewDelegate, UITextFieldDelegat
|
||||
self.number.inputView = keyboard
|
||||
self.check.isEnabled = false
|
||||
|
||||
let ds = RxTableViewSectionedAnimatedDataSource<DateSection<Vehicle>>(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)
|
||||
}
|
||||
self.check(number: numberNormalized, action: .receiveAndSend).subscribe { vehicle in
|
||||
self.updateDetailController(with: vehicle)
|
||||
IHProgressHUD.dismiss()
|
||||
} onError: { error in
|
||||
IHProgressHUD.show(error: error)
|
||||
}
|
||||
}
|
||||
.disposed(by: self.bag)
|
||||
|
||||
func getEvent() -> Single<VehicleEvent> {
|
||||
if let event = LocationManager.lastEvent, (Date().timeIntervalSince1970 - event.date) < 30 {
|
||||
print("Using last event")
|
||||
return Single<VehicleEvent>.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.updateDetailController(with: vehicle)
|
||||
IHProgressHUD.dismiss()
|
||||
}
|
||||
|
||||
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<VehicleEvent> {
|
||||
if case .sendSpecific(let event) = action {
|
||||
return Single.just(event)
|
||||
}
|
||||
|
||||
if let event = LocationManager.lastEvent, (Date().timeIntervalSince1970 - event.date) < 30 {
|
||||
return Single<VehicleEvent>.just(event)
|
||||
} else {
|
||||
return LocationManager.requestCurrentLocation()
|
||||
}
|
||||
}
|
||||
|
||||
func check(number: String, action: EventAction, force: Bool = false) -> Single<Vehicle> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<AudioRecord, AudioRecordCell>!
|
||||
|
||||
let validLetters = Constants.pnLettersMap.keys.map(String.init)
|
||||
|
||||
@ -31,28 +31,11 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
||||
|
||||
self.recorder = Recorder()
|
||||
|
||||
let ds = RxTableViewSectionedAnimatedDataSource<DateSection<AudioRecord>>(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() ?? "")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import Foundation
|
||||
import RxDataSources
|
||||
import SwiftDate
|
||||
|
||||
struct DateSection<T>: AnimatableSectionModelType where T: IdentifiableType, T: Equatable {
|
||||
struct DateSection<T>: AnimatableSectionModelType, Equatable where T: IdentifiableType, T: Equatable {
|
||||
|
||||
var timestamp: Double = 0
|
||||
var header: String
|
||||
|
||||
@ -82,6 +82,7 @@ class Vehicle: Object, Decodable, IdentifiableType {
|
||||
var isRightWheel = RealmOptional<Bool>()
|
||||
@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<VehiclePhoto>()
|
||||
let ownershipPeriods = List<VehicleOwnershipPeriod>()
|
||||
@ -133,7 +134,7 @@ class Vehicle: Object, Decodable, IdentifiableType {
|
||||
self.pts = try container.decodeIfPresent(String.self, forKey: .pts)
|
||||
self.isRightWheel = try container.decode(RealmOptional<Bool>.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 {
|
||||
|
||||
89
AutoCat/Utils/RxRealmDataSource.swift
Normal file
89
AutoCat/Utils/RxRealmDataSource.swift
Normal file
@ -0,0 +1,89 @@
|
||||
import UIKit
|
||||
import RealmSwift
|
||||
import RxDataSources
|
||||
|
||||
class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource where Item: Object & IdentifiableType & Dated, Cell: UITableViewCell & ConfigurableCell, Cell.Item == Item {
|
||||
|
||||
private var tv: UITableView
|
||||
private var data: Results<Item>
|
||||
private var notificationToken: NotificationToken?
|
||||
private var sections: [DateSection<Item>] = []
|
||||
private var cellIdentifier: String
|
||||
|
||||
init(table: UITableView, data: Results<Item>, 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]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user