Adding export CSV from search results

This commit is contained in:
Selim Mustafaev 2022-12-25 18:23:08 +03:00
parent 3ea006c208
commit a60f57b4c6
6 changed files with 132 additions and 34 deletions

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pme-aR-UNJ"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pme-aR-UNJ">
<device id="retina4_7" orientation="portrait" appearance="dark"/> <device id="retina4_7" orientation="portrait" appearance="dark"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -173,17 +173,17 @@
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor"/>
<prototypes> <prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="EventCell" id="QIb-Hv-tvk" customClass="EventCell" customModule="AutoCat" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="EventCell" id="QIb-Hv-tvk" customClass="EventCell" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="0.0" y="50" width="375" height="429.5"/> <rect key="frame" x="0.0" y="50" width="375" height="430.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="QIb-Hv-tvk" id="Ypt-ch-fGT"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="QIb-Hv-tvk" id="Ypt-ch-fGT">
<rect key="frame" x="0.0" y="0.0" width="375" height="429.5"/> <rect key="frame" x="0.0" y="0.0" width="375" height="430.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="HP8-oO-yhP"> <stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="HP8-oO-yhP">
<rect key="frame" x="16" y="8" width="343" height="413.5"/> <rect key="frame" x="16" y="8" width="343" height="414.5"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="k4Z-KM-byE"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="k4Z-KM-byE">
<rect key="frame" x="0.0" y="0.0" width="335" height="413.5"/> <rect key="frame" x="0.0" y="0.0" width="335" height="414.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xcQ-Wz-gJ0"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xcQ-Wz-gJ0">
<rect key="frame" x="0.0" y="0.0" width="335" height="201"/> <rect key="frame" x="0.0" y="0.0" width="335" height="201"/>
@ -192,7 +192,7 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1tQ-zM-6T9"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1tQ-zM-6T9">
<rect key="frame" x="0.0" y="209" width="335" height="204.5"/> <rect key="frame" x="0.0" y="209" width="335" height="205.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/> <fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<color key="textColor" systemColor="secondaryLabelColor"/> <color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@ -200,7 +200,7 @@
</subviews> </subviews>
</stackView> </stackView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="750" verticalHuggingPriority="251" image="person" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="CFI-xa-eLs"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="750" verticalHuggingPriority="251" image="person" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="CFI-xa-eLs">
<rect key="frame" x="343" y="1.5" width="0.0" height="411"/> <rect key="frame" x="343" y="1.5" width="0.0" height="412"/>
</imageView> </imageView>
</subviews> </subviews>
</stackView> </stackView>
@ -386,12 +386,12 @@
<rightBarButtonItems> <rightBarButtonItems>
<barButtonItem image="line.horizontal.3.decrease" catalog="system" id="mvq-Q5-tVc"> <barButtonItem image="line.horizontal.3.decrease" catalog="system" id="mvq-Q5-tVc">
<connections> <connections>
<action selector="onFilter:" destination="UPf-uT-oOr" id="z2g-n9-tJ0"/> <action selector="onFilterTapped:" destination="UPf-uT-oOr" id="eCM-JW-z1h"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
<barButtonItem image="map" catalog="system" id="iVh-uQ-fX5"> <barButtonItem image="map" catalog="system" id="iVh-uQ-fX5">
<connections> <connections>
<action selector="showOnMap:" destination="UPf-uT-oOr" id="PYv-B4-dqB"/> <action selector="onMapTapped:" destination="UPf-uT-oOr" id="ekk-vL-cGI"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
</rightBarButtonItems> </rightBarButtonItems>
@ -535,13 +535,13 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MjS-Hy-iGH"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MjS-Hy-iGH">
<rect key="frame" x="56" y="0.5" width="253.5" height="50"/> <rect key="frame" x="56" y="0.5" width="291" height="50"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/> <fontDescription key="fontDescription" type="system" pointSize="20"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Jgb-TO-YHq"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Jgb-TO-YHq">
<rect key="frame" x="321.5" y="0.5" width="37.5" height="50"/> <rect key="frame" x="359" y="0.5" width="0.0" height="50"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/> <fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" systemColor="secondaryLabelColor"/> <color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>

View File

@ -199,10 +199,7 @@ class CheckController: UIViewController, UITableViewDelegate, UITextFieldDelegat
func shareFile(_ url: URL) { func shareFile(_ url: URL) {
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil) let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
self.present(activityController, animated: true) { self.present(activityController, animated: true)
print("")
}
print("")
} }
// MARK: - Checking new number // MARK: - Checking new number

View File

@ -13,6 +13,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
private var refreshButton: UIBarButtonItem! private var refreshButton: UIBarButtonItem!
private var refreshIndicator: UIBarButtonItem! private var refreshIndicator: UIBarButtonItem!
private var moreActionsButton: UIBarButtonItem?
private let bag = DisposeBag() private let bag = DisposeBag()
private let searchController = UISearchController(searchResultsController: nil) private let searchController = UISearchController(searchResultsController: nil)
@ -35,13 +36,19 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
navigationItem.searchController = searchController navigationItem.searchController = searchController
definesPresentationContext = true definesPresentationContext = true
if #available(iOS 14.0, *) {
setupActionsMenu()
}
self.refreshButton = UIBarButtonItem(image: UIImage(systemName: "arrow.triangle.2.circlepath"), self.refreshButton = UIBarButtonItem(image: UIImage(systemName: "arrow.triangle.2.circlepath"),
style: .plain, style: .plain,
target: self, target: self,
action: #selector(refresh)) action: #selector(refresh))
self.refreshIndicator = UIBarButtonItem(customView: self.pageLoadingIndicator) self.refreshIndicator = UIBarButtonItem(customView: self.pageLoadingIndicator)
#if targetEnvironment(macCatalyst)
self.navigationItem.leftBarButtonItem = self.refreshButton self.navigationItem.leftBarButtonItem = self.refreshButton
#endif
//self.refreshControl.attributedTitle = NSAttributedString(string: "") //self.refreshControl.attributedTitle = NSAttributedString(string: "")
self.refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged) self.refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged)
@ -55,8 +62,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
//.throttle(.seconds(2), scheduler: MainScheduler.instance) //.throttle(.seconds(2), scheduler: MainScheduler.instance)
.debounce(.milliseconds(500), scheduler: MainScheduler.instance) .debounce(.milliseconds(500), scheduler: MainScheduler.instance)
.do(onNext: { _ in .do(onNext: { _ in
self.navigationItem.leftBarButtonItem = self.refreshIndicator self.showProgress()
self.pageLoadingIndicator.startAnimating()
}) })
.flatMap { Api.getVehicles(with: $0, pageToken: self.datasource.pageToken).do(onError: { print($0) }).catchErrorJustReturn(PagedResponse<Vehicle>()) } .flatMap { Api.getVehicles(with: $0, pageToken: self.datasource.pageToken).do(onError: { print($0) }).catchErrorJustReturn(PagedResponse<Vehicle>()) }
.observeOn(MainScheduler.instance) .observeOn(MainScheduler.instance)
@ -68,13 +74,29 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
self.refreshControl.endRefreshing() self.refreshControl.endRefreshing()
self.isLoadingPage = false self.isLoadingPage = false
self.pageLoadingIndicator.stopAnimating() self.pageLoadingIndicator.stopAnimating()
self.navigationItem.leftBarButtonItem = self.refreshButton self.hideProgress()
}) })
.bind(to: self.datasource.data) .bind(to: self.datasource.data)
.disposed(by: self.bag) .disposed(by: self.bag)
} }
} }
func showProgress() {
navigationItem.leftBarButtonItem = self.refreshIndicator
pageLoadingIndicator.startAnimating()
moreActionsButton?.isEnabled = false
}
func hideProgress() {
#if targetEnvironment(macCatalyst)
navigationItem.leftBarButtonItem = self.refreshButton
#else
navigationItem.leftBarButtonItem = nil
#endif
moreActionsButton?.isEnabled = true
}
// FIXME: Code duplication // FIXME: Code duplication
func updateDetailController(with vehicle: Vehicle) { func updateDetailController(with vehicle: Vehicle) {
if let splitViewController = self.view.window?.rootViewController as? UISplitViewController if let splitViewController = self.view.window?.rootViewController as? UISplitViewController
@ -108,13 +130,43 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
self.filterRelay.accept(self.filter) self.filterRelay.accept(self.filter)
} }
// MARK: - // MARK: NavigationBar actions
// @IBAction func onUpdate(_ sender: UIBarButtonItem) { @available(iOS 14.0, *)
// self.refresh(sender) func setupActionsMenu() {
// }
@IBAction func onFilter(_ sender: UIBarButtonItem) { let menu = UIMenu(children: [
UIAction(title: NSLocalizedString("Filter results", comment: ""),
image: UIImage(systemName: "line.horizontal.3.decrease"),
handler: { _ in self.showFilter() }),
UIAction(title: NSLocalizedString("Show on map", comment: ""),
image: UIImage(systemName: "map"),
handler: { _ in self.showOnMap() }),
UIAction(title: NSLocalizedString("Export", comment: ""),
image: UIImage(systemName: "square.and.arrow.up"),
handler: { _ in self.exportSearchResults() })
])
let menuBarButton = UIBarButtonItem(title: nil, image: UIImage(systemName: "ellipsis"), primaryAction: nil, menu: menu)
self.navigationItem.rightBarButtonItems = [menuBarButton]
self.moreActionsButton = menuBarButton
}
@IBAction func onFilterTapped(_ sender: UIBarButtonItem) {
showFilter()
}
@IBAction func onMapTapped(_ sender: UIBarButtonItem) {
showOnMap()
}
@objc func refresh(_ sender: AnyObject) {
self.showMapButton.isEnabled = false
self.datasource.reset()
self.filterRelay.accept(self.filter)
}
func showFilter() {
let sb = UIStoryboard(name: "Main", bundle: nil) let sb = UIStoryboard(name: "Main", bundle: nil)
let controller = sb.instantiateViewController(identifier: "FiltersController") as FiltersController let controller = sb.instantiateViewController(identifier: "FiltersController") as FiltersController
controller.filter = self.filter controller.filter = self.filter
@ -127,7 +179,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
self.navigationController?.pushViewController(controller, animated: true) self.navigationController?.pushViewController(controller, animated: true)
} }
@IBAction func showOnMap(_ sender: UIBarButtonItem) { func showOnMap() {
let sb = UIStoryboard(name: "Main", bundle: nil) let sb = UIStoryboard(name: "Main", bundle: nil)
let controller = sb.instantiateViewController(identifier: "GlobalEventsNavigation") as UINavigationController let controller = sb.instantiateViewController(identifier: "GlobalEventsNavigation") as UINavigationController
if let eventsVC = controller.viewControllers.first as? GlobalEventsController { if let eventsVC = controller.viewControllers.first as? GlobalEventsController {
@ -135,14 +187,38 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
} }
controller.modalPresentationStyle = .fullScreen controller.modalPresentationStyle = .fullScreen
//self.navigationController?.pushViewController(controller, animated: true)
self.present(controller, animated: true) self.present(controller, animated: true)
} }
@objc func refresh(_ sender: AnyObject) { func exportSearchResults() {
self.showMapButton.isEnabled = false showProgress()
self.datasource.reset()
self.filterRelay.accept(self.filter) Api.getVehicles(with: filter, pageSize: 0)
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { resp in
self.hideProgress()
let newLine = "\r\n"
var csvString = Vehicle.csvHeader + newLine
for vehicle in resp.items {
csvString.append(vehicle.csvLine)
csvString.append(newLine)
}
do {
let tmpUrl = FileManager.default.tmpUrl(name: "search", ext: "csv")
try csvString.write(to: tmpUrl, atomically: true, encoding: .utf8)
let activityController = UIActivityViewController(activityItems: [tmpUrl], applicationActivities: nil)
self.present(activityController, animated: true)
} catch {
HUD.show(error: error)
}
}, onError: { error in
self.hideProgress()
HUD.show(error: error)
})
.disposed(by: bag)
} }
// MARK: - UITableViewDelegate // MARK: - UITableViewDelegate

View File

@ -139,12 +139,18 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Events" = "События"; "Events" = "События";
/* No comment provided by engineer. */
"Export" = "Экспорт";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Export history as" = "Экспортировать историю как"; "Export history as" = "Экспортировать историю как";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Filter check history" = "Фильтр истории поиска"; "Filter check history" = "Фильтр истории поиска";
/* No comment provided by engineer. */
"Filter results" = "Фильтры";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"From" = "С"; "From" = "С";

View File

@ -364,13 +364,31 @@ public class Vehicle: Object, Decodable, Identifiable, Differentiable, Cloneable
// MARK: - Exportable // MARK: - Exportable
public static var csvHeader: String { public static var csvHeader: String {
return "Model, Color, Year, Plate Number, VIN, STS, PTS, Added Date, Updated date" return "Model, Color, Year, Plate Number, VIN, STS, PTS, Added Date, Updated date, Locations"
} }
public var csvLine: String { public var csvLine: String {
let model = self.brand?.name?.original ?? "<unknown>" let model = self.brand?.name?.original ?? "<unknown>"
let added = self.formatter.string(from: Date(timeIntervalSince1970: self.addedDate)) let added = self.formatter.string(from: Date(timeIntervalSince1970: self.addedDate))
let updated = self.formatter.string(from: Date(timeIntervalSince1970: self.updatedDate)) let updated = self.formatter.string(from: Date(timeIntervalSince1970: self.updatedDate))
return String(format: "\"%@\", %@, %d, %@, %@, %@, %@, \"%@\", \"%@\"", model, self.color ?? "", self.year, self.number, self.vin1 ?? "", self.sts ?? "", self.pts ?? "", added, updated)
var eventsString = ""
for event in events {
let location = event.address ?? "lat: \(event.latitude), lon: \(event.longitude)"
let date = formatter.string(from: Date(timeIntervalSince1970: event.date))
eventsString.append(location + "; " + date + "\r\n")
}
return String(format: "\"%@\", %@, %d, %@, %@, %@, %@, \"%@\", \"%@\", \"%@\"",
model,
self.color ?? "",
self.year,
self.number,
self.vin1 ?? "",
self.sts ?? "",
self.pts ?? "",
added,
updated,
eventsString)
} }
} }

View File

@ -198,8 +198,9 @@ public class Api {
return self.makeBodyRequest(api: "user/signup", body: body) return self.makeBodyRequest(api: "user/signup", body: body)
} }
public static func getVehicles(with filter: Filter, pageToken: String? = nil) -> Single<PagedResponse<Vehicle>> { public static func getVehicles(with filter: Filter, pageToken: String? = nil, pageSize: Int = 50) -> Single<PagedResponse<Vehicle>> {
var params = filter.queryDictionary() var params = filter.queryDictionary()
params["pageSize"] = String(pageSize)
if let token = pageToken { if let token = pageToken {
params["pageToken"] = token; params["pageToken"] = token;
} }