474 lines
21 KiB
Swift
474 lines
21 KiB
Swift
import UIKit
|
|
import MagazineLayout
|
|
import Kingfisher
|
|
import LinkPresentation
|
|
import RealmSwift
|
|
|
|
enum ReportSection: Int, CaseIterable, CustomStringConvertible {
|
|
case header = 0
|
|
case general = 1
|
|
case identifiers = 2
|
|
case engine = 3
|
|
case photos = 4
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .header: return "Header"
|
|
case .general: return "General"
|
|
case .identifiers: return "Identifiers"
|
|
case .engine: return "Engine"
|
|
case .photos: return "Photos"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ReportGeneralSection: Int, CaseIterable, CustomStringConvertible {
|
|
case year = 0
|
|
case color = 1
|
|
case category = 2
|
|
case wheelPosition = 3
|
|
case japanese = 4
|
|
case owners = 5
|
|
case events = 6
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .year: return "Year"
|
|
case .color: return "Color"
|
|
case .category: return "Category"
|
|
case .wheelPosition: return "Steering wheel position"
|
|
case .japanese: return "Japanese"
|
|
case .owners: return "Owners (from PTS)"
|
|
case .events: return "Events"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ReportIdSection: Int, CaseIterable, CustomStringConvertible {
|
|
case number = 0
|
|
case vin = 1
|
|
case sts = 2
|
|
case pts = 3
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .number: return "Number"
|
|
case .vin: return "VIN"
|
|
case .pts: return "PTS"
|
|
case .sts: return "STS"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ReportEngineSection: Int, CaseIterable, CustomStringConvertible {
|
|
case number = 0
|
|
case fuelType = 1
|
|
case volume = 2
|
|
case powerHp = 3
|
|
case powerKw = 4
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .number: return "Number"
|
|
case .fuelType: return "Fuel type"
|
|
case .volume: return "Volume (cm³)"
|
|
case .powerHp: return "Power (HP)"
|
|
case .powerKw: return "Power (kw)"
|
|
}
|
|
}
|
|
}
|
|
|
|
class ReportController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateMagazineLayout, MediaBrowserViewControllerDataSource, MediaBrowserViewControllerDelegate, UIActivityItemSource {
|
|
|
|
@IBOutlet weak var collection: UICollectionView!
|
|
@IBOutlet weak var actionBarItem: UIBarButtonItem!
|
|
@IBOutlet weak var copyBarItem: UIBarButtonItem!
|
|
|
|
private let fullWidth = MagazineLayoutItemSizeMode(widthMode: .fullWidth(respectsHorizontalInsets: true), heightMode: .dynamic)
|
|
private var reportImageUrl: URL?
|
|
|
|
var vehicle: Vehicle? {
|
|
didSet {
|
|
loadViewIfNeeded()
|
|
self.collection.reloadData()
|
|
self.navigationController?.setNavigationBarHidden(self.vehicle == nil, animated: false)
|
|
}
|
|
}
|
|
|
|
private var notificationToken: NotificationToken?
|
|
var number: String? {
|
|
didSet {
|
|
if let realm = try? Realm(), let num = number {
|
|
let vehicles = realm.objects(Vehicle.self).filter("number = %@", num)
|
|
self.notificationToken?.invalidate()
|
|
self.notificationToken = vehicles.observe { _ in self.vehicle = vehicles.first }
|
|
} else {
|
|
self.vehicle = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
self.collection.collectionViewLayout = MagazineLayout()
|
|
let nib = UINib(nibName: "SectionHeader", bundle: nil)
|
|
self.collection.register(nib, forSupplementaryViewOfKind: MagazineLayout.SupplementaryViewKind.sectionHeader, withReuseIdentifier: "SectionHeader")
|
|
|
|
if let vehicle = self.vehicle {
|
|
let urls = Array(vehicle.photos.compactMap { URL(string: $0.url) })
|
|
let prefetcher = ImagePrefetcher(urls: urls)
|
|
prefetcher.start()
|
|
}
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
|
|
|
|
self.navigationController?.setNavigationBarHidden(self.vehicle == nil, animated: animated)
|
|
|
|
switch ad.quickAction {
|
|
case .check:
|
|
self.dismiss(animated: false, completion: nil)
|
|
default:
|
|
break
|
|
}
|
|
|
|
self.collection.reloadData()
|
|
}
|
|
|
|
// MARK: - UICollectionViewDataSource
|
|
|
|
func numberOfSections(in collectionView: UICollectionView) -> Int {
|
|
guard self.vehicle != nil else { return 0 }
|
|
return ReportSection.allCases.count
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
guard let vehicle = self.vehicle else { return 0 }
|
|
guard let section = ReportSection(rawValue: section) else { return 0 }
|
|
|
|
switch section {
|
|
case .header: return 1
|
|
case .general: return ReportGeneralSection.allCases.count
|
|
case .identifiers: return ReportIdSection.allCases.count
|
|
case .engine: return ReportEngineSection.allCases.count
|
|
case .photos: return vehicle.photos.count
|
|
}
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
guard let section = ReportSection(rawValue: indexPath.section) else { return UICollectionViewCell() }
|
|
guard let vehicle = self.vehicle else { return UICollectionViewCell() }
|
|
|
|
switch section {
|
|
case .header:
|
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VehicleHeaderCell", for: indexPath) as? VehicleHeaderCell
|
|
cell?.configure(with: vehicle)
|
|
return cell ?? UICollectionViewCell()
|
|
case .general:
|
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VehicleTextParamCell", for: indexPath) as? VehicleTextParamCell
|
|
if let generalSection = ReportGeneralSection(rawValue: indexPath.item) {
|
|
switch generalSection {
|
|
case .year:
|
|
cell?.configure(param: generalSection.description, value: String(vehicle.year))
|
|
break
|
|
case .color:
|
|
cell?.configure(param: generalSection.description, value: vehicle.color ?? "<unknown>")
|
|
break
|
|
case .category:
|
|
cell?.configure(param: generalSection.description, value: vehicle.category ?? "<unknown>")
|
|
break
|
|
case .wheelPosition:
|
|
var position = "Unknown"
|
|
if let rightWheel = vehicle.isRightWheel.value {
|
|
position = rightWheel ? "Right" : "Left"
|
|
}
|
|
cell?.configure(param: generalSection.description, value: position)
|
|
break
|
|
case .japanese:
|
|
cell?.configure(param: generalSection.description, value: vehicle.isJapanese ? "Yes" : "No")
|
|
break
|
|
case .owners:
|
|
cell?.configure(param: generalSection.description, value: String(vehicle.ownershipPeriods.count))
|
|
break
|
|
case .events:
|
|
cell?.configure(param: generalSection.description, value: String(vehicle.events.count))
|
|
}
|
|
}
|
|
return cell ?? UICollectionViewCell()
|
|
case .identifiers:
|
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VehicleTextParamCell", for: indexPath) as? VehicleTextParamCell
|
|
if let idSection = ReportIdSection(rawValue: indexPath.item) {
|
|
switch idSection {
|
|
case .number:
|
|
var num = vehicle.getNumber()
|
|
if vehicle.outdated, let current = vehicle.currentNumber {
|
|
num = "\(vehicle.getNumber()) (\(current))"
|
|
}
|
|
cell?.configure(param: idSection.description, value: num)
|
|
break
|
|
case .vin:
|
|
cell?.configure(param: idSection.description, value: vehicle.vin1 ?? "<unknown>")
|
|
break
|
|
case .sts:
|
|
cell?.configure(param: idSection.description, value: vehicle.sts ?? "<unknown>")
|
|
break
|
|
case .pts:
|
|
cell?.configure(param: idSection.description, value: vehicle.pts ?? "<unknown>")
|
|
break
|
|
}
|
|
}
|
|
return cell ?? UICollectionViewCell()
|
|
case .engine:
|
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VehicleTextParamCell", for: indexPath) as? VehicleTextParamCell
|
|
if let engineSection = ReportEngineSection(rawValue: indexPath.item), let engine = vehicle.engine {
|
|
switch engineSection {
|
|
case .number:
|
|
cell?.configure(param: engineSection.description, value: engine.number ?? "<unknown>")
|
|
break
|
|
case .fuelType:
|
|
cell?.configure(param: engineSection.description, value: engine.fuelType ?? "<unknown>")
|
|
break
|
|
case .volume:
|
|
cell?.configure(param: engineSection.description, value: String(engine.volume.value ?? 0))
|
|
break
|
|
case .powerHp:
|
|
cell?.configure(param: engineSection.description, value: String(engine.powerHp))
|
|
break
|
|
case .powerKw:
|
|
cell?.configure(param: engineSection.description, value: String(engine.powerKw))
|
|
break
|
|
}
|
|
}
|
|
return cell ?? UICollectionViewCell()
|
|
case .photos:
|
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VehiclePhotoCell", for: indexPath) as? VehiclePhotoCell
|
|
let photo = vehicle.photos[indexPath.item]
|
|
cell?.configure(with: photo)
|
|
return cell ?? UICollectionViewCell()
|
|
}
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
|
{
|
|
guard let section = ReportSection(rawValue: indexPath.section) else { return UICollectionReusableView() }
|
|
|
|
if let sectionHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "SectionHeader", for: indexPath) as? SectionHeader {
|
|
sectionHeader.configure(with: section)
|
|
return sectionHeader
|
|
}
|
|
return UICollectionReusableView()
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
if indexPath.section == ReportSection.photos.rawValue {
|
|
let mediaBrowser = MediaBrowserViewController(index: indexPath.item, dataSource: self, delegate: self)
|
|
mediaBrowser.shouldShowTitle = true
|
|
mediaBrowser.title = self.vehicle?.photos[indexPath.item].description
|
|
present(mediaBrowser, animated: true, completion: nil)
|
|
}
|
|
|
|
if indexPath.section == ReportSection.general.rawValue {
|
|
let sb = UIStoryboard(name: "Main", bundle: nil)
|
|
if indexPath.row == ReportGeneralSection.owners.rawValue {
|
|
let controller = sb.instantiateViewController(identifier: "OwnersController") as OwnersController
|
|
controller.owners = self.vehicle?.ownershipPeriods.toArray() ?? []
|
|
self.navigationController?.pushViewController(controller, animated: true)
|
|
}
|
|
else if indexPath.row == ReportGeneralSection.events.rawValue {
|
|
let controller = sb.instantiateViewController(identifier: "EventsController") as EventsController
|
|
controller.vehicle = self.vehicle
|
|
self.navigationController?.pushViewController(controller, animated: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - UICollectionViewDelegateMagazineLayout
|
|
|
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeModeForItemAt indexPath: IndexPath) -> MagazineLayoutItemSizeMode
|
|
{
|
|
guard let section = ReportSection(rawValue: indexPath.section) else { return self.fullWidth }
|
|
switch section {
|
|
case .header: return self.fullWidth
|
|
case .general: return self.fullWidth
|
|
case .identifiers: return self.fullWidth
|
|
case .engine: return self.fullWidth
|
|
case .photos:
|
|
let wMode: MagazineLayoutItemWidthMode = self.traitCollection.horizontalSizeClass != .compact ? .thirdWidth : .halfWidth
|
|
return .init(widthMode: wMode, heightMode: .dynamic)
|
|
}
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, visibilityModeForHeaderInSectionAtIndex index: Int) -> MagazineLayoutHeaderVisibilityMode
|
|
{
|
|
guard let section = ReportSection(rawValue: index) else { return .hidden }
|
|
switch section {
|
|
case .header: return .hidden
|
|
case .general: return .visible(heightMode: .dynamic)
|
|
case .identifiers: return .visible(heightMode: .dynamic)
|
|
case .engine: return .visible(heightMode: .dynamic)
|
|
case .photos: return .visible(heightMode: .dynamic)
|
|
}
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, visibilityModeForFooterInSectionAtIndex index: Int) -> MagazineLayoutFooterVisibilityMode
|
|
{
|
|
return .hidden
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, visibilityModeForBackgroundInSectionAtIndex index: Int) -> MagazineLayoutBackgroundVisibilityMode
|
|
{
|
|
return .hidden
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, horizontalSpacingForItemsInSectionAtIndex index: Int) -> CGFloat
|
|
{
|
|
return 16
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, verticalSpacingForElementsInSectionAtIndex index: Int) -> CGFloat
|
|
{
|
|
return index == ReportSection.photos.rawValue ? 16 : 0
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetsForSectionAtIndex index: Int) -> UIEdgeInsets
|
|
{
|
|
return index == ReportSection.photos.rawValue ? UIEdgeInsets(top: 0, left: 0, bottom: 16, right: 0) : .zero
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetsForItemsInSectionAtIndex index: Int) -> UIEdgeInsets
|
|
{
|
|
return index == ReportSection.photos.rawValue ? UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) : .zero
|
|
}
|
|
|
|
// MARK: - MediaBrowserViewControllerDataSource & MediaBrowserViewControllerDelegate
|
|
|
|
func numberOfItems(in mediaBrowser: MediaBrowserViewController) -> Int {
|
|
guard let images = self.vehicle?.photos else { return 0 }
|
|
return images.count
|
|
}
|
|
|
|
func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, imageAt index: Int, completion: @escaping MediaBrowserViewControllerDataSource.CompletionBlock) {
|
|
guard let images = self.vehicle?.photos, let url = URL(string: images[index].url) else {
|
|
completion(index, nil, ZoomScale.default, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Image not found"]))
|
|
return
|
|
}
|
|
|
|
KingfisherManager.shared.retrieveImage(with: url) { result in
|
|
switch result {
|
|
case .success(let res):
|
|
completion(index, res.image, ZoomScale.default, nil)
|
|
break
|
|
case .failure(let error):
|
|
completion(index, nil, ZoomScale.default, error)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, didChangeFocusTo index: Int) {
|
|
guard let photo = self.vehicle?.photos[index] else { return }
|
|
mediaBrowser.title = photo.description
|
|
}
|
|
|
|
// MARK: - Sharing
|
|
|
|
@IBAction func onShare(_ sender: UIBarButtonItem) {
|
|
guard let vehicle = self.vehicle else { return }
|
|
|
|
let sheet = UIAlertController(title: "Share report", message: nil, preferredStyle: .actionSheet)
|
|
sheet.popoverPresentationController?.barButtonItem = self.actionBarItem
|
|
|
|
let cancel = UIAlertAction(title: "Cancel", style: .cancel) { _ in sheet.dismiss(animated: true, completion: nil) }
|
|
let shareImage = UIAlertAction(title: "As one image", style: .default) { _ in
|
|
let image = vehicle.reportImage(width: self.collection.contentSize.width)
|
|
|
|
do {
|
|
let fm = FileManager.default
|
|
let documentDirectory = try fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
|
|
let fileURL = documentDirectory.appendingPathComponent("report.png")
|
|
if let imageData = image.pngData() {
|
|
try imageData.write(to: fileURL)
|
|
self.reportImageUrl = fileURL
|
|
}
|
|
|
|
let controller = UIActivityViewController(activityItems: [self], applicationActivities: nil)
|
|
controller.popoverPresentationController?.barButtonItem = sender
|
|
self.present(controller, animated: true)
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
let shareTextAndImage = UIAlertAction(title: "As text and photos", style: .default) { _ in
|
|
guard let vehicle = self.vehicle else { return }
|
|
var items: [Any] = [vehicle.reportText()]
|
|
for photo in vehicle.photos {
|
|
if let url = URL(string: photo.url) {
|
|
if let image = ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) {
|
|
items.append(image)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
let controller = UIActivityViewController(activityItems: items, applicationActivities: nil)
|
|
controller.popoverPresentationController?.barButtonItem = sender
|
|
self.present(controller, animated: true)
|
|
}
|
|
|
|
let shareLink = UIAlertAction(title: "As link", style: .default) { _ in
|
|
guard let vehicle = self.vehicle else { return }
|
|
|
|
if let jwt = try? JWT<EmptyPayload>.generate(for: vehicle.getNumber()), let url = URL(string: Constants.reportLinkBaseURL + "?token=" + jwt) {
|
|
let controller = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
|
controller.popoverPresentationController?.barButtonItem = sender
|
|
self.present(controller, animated: true)
|
|
}
|
|
}
|
|
|
|
sheet.addAction(shareImage)
|
|
sheet.addAction(shareTextAndImage)
|
|
sheet.addAction(shareLink)
|
|
sheet.addAction(cancel)
|
|
self.present(sheet, animated: true, completion: nil)
|
|
}
|
|
|
|
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
|
return UIImage()
|
|
}
|
|
|
|
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
|
return self.reportImageUrl
|
|
}
|
|
|
|
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
|
|
guard let url = self.reportImageUrl else { return nil }
|
|
|
|
let metadata = LPLinkMetadata()
|
|
metadata.title = self.vehicle?.getNumber()
|
|
metadata.originalURL = url
|
|
metadata.url = url
|
|
metadata.imageProvider = NSItemProvider.init(contentsOf: url)
|
|
metadata.iconProvider = NSItemProvider.init(contentsOf: url)
|
|
return metadata
|
|
}
|
|
|
|
// MARK: - Copy
|
|
|
|
@IBAction func onCopy(_ sender: UIBarButtonItem) {
|
|
let sheet = UIAlertController(title: "Copy to pasteboard", message: nil, preferredStyle: .actionSheet)
|
|
sheet.popoverPresentationController?.barButtonItem = self.copyBarItem
|
|
let cancel = UIAlertAction(title: "Cancel", style: .cancel) { _ in sheet.dismiss(animated: true, completion: nil) }
|
|
let copyPlateNumber = UIAlertAction(title: "Plate number", style: .default) { _ in UIPasteboard.general.string = self.vehicle?.getNumber() }
|
|
let copyVin = UIAlertAction(title: "VIN", style: .default) { _ in UIPasteboard.general.string = self.vehicle?.vin1 }
|
|
sheet.addAction(copyPlateNumber)
|
|
sheet.addAction(copyVin)
|
|
sheet.addAction(cancel)
|
|
self.present(sheet, animated: true, completion: nil)
|
|
}
|
|
}
|