From 6bf0ad8d1237d70a44392233f3ac8a30d42b8233 Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Fri, 3 Apr 2020 21:30:54 +0300 Subject: [PATCH] Search plate numbers --- AutoCat.xcodeproj/project.pbxproj | 20 +++++ AutoCat/Base.lproj/Main.storyboard | 92 ++++++++++++++++++++-- AutoCat/Controllers/CheckController.swift | 38 ++------- AutoCat/Controllers/SearchController.swift | 82 +++++++++++++++++++ AutoCat/Extensions/Dated.swift | 40 ++++++++++ AutoCat/Models/DateSection.swift | 1 - AutoCat/Models/Filter.swift | 5 ++ AutoCat/Utils/Api.swift | 22 ++++-- AutoCat/Views/FlagLayer.swift | 1 - AutoCat/Views/PlateView.swift | 15 +++- 10 files changed, 267 insertions(+), 49 deletions(-) create mode 100644 AutoCat/Controllers/SearchController.swift create mode 100644 AutoCat/Extensions/Dated.swift create mode 100644 AutoCat/Models/Filter.swift diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index c3e0528..4c1a801 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -31,6 +31,9 @@ 7A11474923FF2B2D00B424AF /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474823FF2B2D00B424AF /* Response.swift */; }; 7A11474B23FF368B00B424AF /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474A23FF368B00B424AF /* Settings.swift */; }; 7A11474E23FFEE8800B424AF /* SVProgressHUD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A11474D23FFEE8800B424AF /* SVProgressHUD.framework */; }; + 7A3F07AB24360DC800E59687 /* Dated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AA24360DC800E59687 /* Dated.swift */; }; + 7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AC2436350B00E59687 /* SearchController.swift */; }; + 7A3F07AF24366DF900E59687 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AE24366DF900E59687 /* Filter.swift */; }; 7A530B78240010D900CBFE6E /* InputMask in Frameworks */ = {isa = PBXBuildFile; productRef = 7A530B77240010D900CBFE6E /* InputMask */; }; 7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7924001D3300CBFE6E /* CheckController.swift */; }; 7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */; }; @@ -71,6 +74,9 @@ 7A11474823FF2B2D00B424AF /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; 7A11474A23FF368B00B424AF /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; 7A11474D23FFEE8800B424AF /* SVProgressHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SVProgressHUD.framework; path = Carthage/Build/iOS/SVProgressHUD.framework; sourceTree = ""; }; + 7A3F07AA24360DC800E59687 /* Dated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dated.swift; sourceTree = ""; }; + 7A3F07AC2436350B00E59687 /* SearchController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchController.swift; sourceTree = ""; }; + 7A3F07AE24366DF900E59687 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; 7A530B7924001D3300CBFE6E /* CheckController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckController.swift; sourceTree = ""; }; 7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleCell.swift; sourceTree = ""; }; 7A530B7F2401803A00CBFE6E /* Vehicle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = ""; }; @@ -137,6 +143,7 @@ 7A1146FF23FDE7E500B424AF /* AutoCat */ = { isa = PBXGroup; children = ( + 7A3F07A924360D9100E59687 /* Extensions */, 7A6DD90424326788009DE740 /* Fonts */, 7A6DD901242BF48D009DE740 /* Views */, 7A530B7C24017FBE00CBFE6E /* Cells */, @@ -162,6 +169,7 @@ 7A11471923FE839000B424AF /* AuthController.swift */, 7A530B7924001D3300CBFE6E /* CheckController.swift */, 7AEFE727240455E200910EB7 /* SettingsController.swift */, + 7A3F07AC2436350B00E59687 /* SearchController.swift */, ); path = Controllers; sourceTree = ""; @@ -191,6 +199,7 @@ 7A530B7F2401803A00CBFE6E /* Vehicle.swift */, 7A0516192414FF0900FC55AC /* DateSection.swift */, 7A6DD90D24337930009DE740 /* PlateNumber.swift */, + 7A3F07AE24366DF900E59687 /* Filter.swift */, ); path = Models; sourceTree = ""; @@ -204,6 +213,14 @@ name = Frameworks; sourceTree = ""; }; + 7A3F07A924360D9100E59687 /* Extensions */ = { + isa = PBXGroup; + children = ( + 7A3F07AA24360DC800E59687 /* Dated.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 7A530B7C24017FBE00CBFE6E /* Cells */ = { isa = PBXGroup; children = ( @@ -362,10 +379,12 @@ 7A530B802401803A00CBFE6E /* Vehicle.swift in Sources */, 7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */, 7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */, + 7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */, 7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */, 7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */, 7A6DD90E24337930009DE740 /* PlateNumber.swift in Sources */, 7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */, + 7A3F07AB24360DC800E59687 /* Dated.swift in Sources */, 7A11474923FF2B2D00B424AF /* Response.swift in Sources */, 7A11471823FDEBFA00B424AF /* ReportController.swift in Sources */, 7A11471A23FE839000B424AF /* AuthController.swift in Sources */, @@ -382,6 +401,7 @@ 7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */, 7A05161A2414FF0900FC55AC /* DateSection.swift in Sources */, 7A11474B23FF368B00B424AF /* Settings.swift in Sources */, + 7A3F07AF24366DF900E59687 /* Filter.swift in Sources */, 7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/AutoCat/Base.lproj/Main.storyboard b/AutoCat/Base.lproj/Main.storyboard index b2ee95d..0fcf792 100644 --- a/AutoCat/Base.lproj/Main.storyboard +++ b/AutoCat/Base.lproj/Main.storyboard @@ -181,21 +181,84 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + @@ -354,7 +417,7 @@ - + @@ -485,6 +548,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/AutoCat/Controllers/CheckController.swift b/AutoCat/Controllers/CheckController.swift index af3a4d2..5f66052 100644 --- a/AutoCat/Controllers/CheckController.swift +++ b/AutoCat/Controllers/CheckController.swift @@ -19,6 +19,8 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener { override func viewDidLoad() { super.viewDidLoad() + guard let realm = try? Realm() else { return } + self.maskFieldDelegate.primaryMaskFormat = "[A][000][AA] [009]" self.maskFieldDelegate.listener = self self.number.delegate = self.maskFieldDelegate @@ -41,36 +43,11 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener { .subscribe(onNext: self.updateDetailController(with:)) .disposed(by: self.bag) - let now = Date() - let monthStart = now.dateAtStartOf(.month) - let realm = try! Realm() - - Observable.collection(from: realm.objects(Vehicle.self).sorted(byKeyPath: "addedDate", ascending: false)).map { (vehicles: Results) -> [DateSection] in - var sections: [TimeInterval: [Vehicle]] = [:] - for vehicle in vehicles { - let date = Date(timeIntervalSince1970: vehicle.addedDate/1000) - - var key = date.dateAtStartOf(.day).timeIntervalSince1970 - if date.isBeforeDate(monthStart, orEqual: false, granularity: .day) { - key = date.dateAtStartOf(.month).timeIntervalSince1970 - } - - if sections[key] == nil { - sections[key] = [vehicle] - } else { - sections[key]?.append(vehicle) - } - } - - var sectionsArray: [DateSection] = [] - for (timestamp, vehicles) in sections { - sectionsArray.append(DateSection(timestamp: timestamp, items: vehicles)) - } - - return sectionsArray.sorted { $0.timestamp > $1.timestamp } - } - .bind(to: self.history.rx.items(dataSource: ds)) - .disposed(by: self.bag) + 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) } override func viewWillAppear(_ animated: Bool) { @@ -109,7 +86,6 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener { func textField(_ textField: UITextField, didFillMandatoryCharacters complete: Bool, didExtractValue value: String) { self.check.isEnabled = complete - print(value) } func textFieldShouldReturn(_ textField: UITextField) -> Bool { diff --git a/AutoCat/Controllers/SearchController.swift b/AutoCat/Controllers/SearchController.swift new file mode 100644 index 0000000..0cbb72d --- /dev/null +++ b/AutoCat/Controllers/SearchController.swift @@ -0,0 +1,82 @@ +import UIKit +import RxDataSources +import RxSwift +import RxCocoa + +class SearchController: UIViewController, UISearchResultsUpdating { + + @IBOutlet weak var tableView: UITableView! + + let bag = DisposeBag() + let searchController = UISearchController(searchResultsController: nil) + + var filterRelay = BehaviorRelay(value: Filter()) + var filter = Filter() + + override func viewDidLoad() { + super.viewDidLoad() + + searchController.searchResultsUpdater = self + searchController.obscuresBackgroundDuringPresentation = false + searchController.searchBar.placeholder = "Search plate numbers" + navigationItem.searchController = searchController + definesPresentationContext = true + + 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() + } + }) + + ds.titleForHeaderInSection = { dataSourse, index in + return dataSourse.sectionModels[index].header + } + + self.tableView.rx.modelSelected(Vehicle.self) + .subscribe(onNext: self.updateDetailController(with:)) + .disposed(by: self.bag) + + self.filterRelay + //.throttle(.seconds(2), scheduler: MainScheduler.instance) + .debounce(.milliseconds(500), scheduler: MainScheduler.instance) + .flatMap(Api.getVehicles) + .observeOn(MainScheduler.instance) + .do(onNext: { self.navigationItem.title = "\($0.count) vehicles found" }) + .map { $0.groupedByDate() } + .bind(to: self.tableView.rx.items(dataSource: ds)) + .disposed(by: self.bag) + } + + // FIXME: Code duplication + func updateDetailController(with vehicle: Vehicle) { + if let splitViewController = self.view.window?.rootViewController as? UISplitViewController + { + var detail: ReportController? + if splitViewController.viewControllers.count == 2 { + detail = splitViewController.viewControllers.last as? ReportController + } else { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + detail = storyboard.instantiateViewController(identifier: "ReportController") + } + + if let detail = detail { + detail.vehicle = vehicle + splitViewController.showDetailViewController(detail, sender: self) + //self.performSegue(withIdentifier: "OpenDetailSegue", sender: self) + } + } + } + + // MARK: - UISearchResultsUpdating + + func updateSearchResults(for searchController: UISearchController) { + let newQuery = searchController.searchBar.text?.uppercased() ?? "" + guard self.filter.searchString != newQuery else { return } + + self.filter.searchString = newQuery + self.filterRelay.accept(self.filter) + } +} diff --git a/AutoCat/Extensions/Dated.swift b/AutoCat/Extensions/Dated.swift new file mode 100644 index 0000000..3776e1f --- /dev/null +++ b/AutoCat/Extensions/Dated.swift @@ -0,0 +1,40 @@ +import Foundation +import RxDataSources +import SwiftDate + +protocol Dated { + var date: Date { get } +} + +extension Vehicle: Dated { + var date: Date { + Date(timeIntervalSince1970: self.addedDate/1000) + } +} + +extension RandomAccessCollection where Element: Dated & IdentifiableType & Equatable { + func groupedByDate() -> [DateSection] { + let now = Date() + let monthStart = now.dateAtStartOf(.month) + var sections: [TimeInterval: [Element]] = [:] + for vehicle in self { + var key = vehicle.date.dateAtStartOf(.day).timeIntervalSince1970 + if vehicle.date.isBeforeDate(monthStart, orEqual: false, granularity: .day) { + key = vehicle.date.dateAtStartOf(.month).timeIntervalSince1970 + } + + if sections[key] == nil { + sections[key] = [vehicle] + } else { + sections[key]?.append(vehicle) + } + } + + var sectionsArray: [DateSection] = [] + for (timestamp, vehicles) in sections { + sectionsArray.append(DateSection(timestamp: timestamp, items: vehicles)) + } + + return sectionsArray.sorted { $0.timestamp > $1.timestamp } + } +} diff --git a/AutoCat/Models/DateSection.swift b/AutoCat/Models/DateSection.swift index f4e27b8..cefadc5 100644 --- a/AutoCat/Models/DateSection.swift +++ b/AutoCat/Models/DateSection.swift @@ -27,7 +27,6 @@ struct DateSection: AnimatableSectionModelType where T: IdentifiableType, T: let weekStart = now.dateAtStartOf(.weekOfMonth) let date = Date(timeIntervalSince1970: timestamp) - print("Date: \(date)") if date.isToday { self.header = "Today" } diff --git a/AutoCat/Models/Filter.swift b/AutoCat/Models/Filter.swift new file mode 100644 index 0000000..d50157e --- /dev/null +++ b/AutoCat/Models/Filter.swift @@ -0,0 +1,5 @@ +import Foundation + +struct Filter { + var searchString = "" +} diff --git a/AutoCat/Utils/Api.swift b/AutoCat/Utils/Api.swift index a7b4717..3a3f765 100644 --- a/AutoCat/Utils/Api.swift +++ b/AutoCat/Utils/Api.swift @@ -8,23 +8,27 @@ class Api { return NSError(domain: "", code: code, userInfo: [NSLocalizedDescriptionKey: msg, NSLocalizedRecoverySuggestionErrorKey: suggestion]) } - private static func createRequest(api: String, method: String, body: [String: Any]? = nil) -> URLRequest? { - guard let url = URL(string: baseUrl + api) else { return nil } + private static func createRequest(api: String, method: String, body: [String: T]? = nil) -> URLRequest? where T: LosslessStringConvertible { + guard var urlComponents = URLComponents(string: baseUrl + api) else { return nil } - var request = URLRequest(url: url) + if let body = body, method.uppercased() == "GET" { + urlComponents.queryItems = body.map { URLQueryItem(name: $0, value: String($1)) } + } + + var request = URLRequest(url: urlComponents.url!) request.httpMethod = method request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("application/json", forHTTPHeaderField: "Accept") request.addValue("Bearer " + Settings.shared.user.token, forHTTPHeaderField: "Authorization") - if let body = body { + if let body = body, method.uppercased() != "GET" { request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) } return request } - private static func makeRequest(api: String, method: String, body: [String: Any]? = nil) -> Observable where T: Decodable { + private static func makeRequest(api: String, method: String, body: [String: U]? = nil) -> Observable where T: Decodable, U: LosslessStringConvertible { guard let request = self.createRequest(api: api, method: method, body: body) else { return Observable.error(self.genError("Error creating request", suggestion: "")) } @@ -59,8 +63,12 @@ class Api { return self.makeRequest(api: "user/signup", method: "POST", body: body) } - public static func getVehicles() -> Observable<[Vehicle]> { - return self.makeRequest(api: "vehicles", method: "GET") + public static func getVehicles(with filter: Filter) -> Observable<[Vehicle]> { + let body = [ + "limit": "0", // Unlimited + "query": filter.searchString + ] + return self.makeRequest(api: "vehicles", method: "GET", body: body) } public static func checkVehicle(by number: String) -> Observable { diff --git a/AutoCat/Views/FlagLayer.swift b/AutoCat/Views/FlagLayer.swift index 7246cc4..2b87d06 100644 --- a/AutoCat/Views/FlagLayer.swift +++ b/AutoCat/Views/FlagLayer.swift @@ -8,7 +8,6 @@ class FlagLayer: CALayer { let red = CGColor(srgbRed: 213/256.0, green: 43/256.0, blue: 30/256.0, alpha: 1) override func draw(in ctx: CGContext) { - print("Draw in bounds: \(bounds)") ctx.saveGState() super.draw(in: ctx) diff --git a/AutoCat/Views/PlateView.swift b/AutoCat/Views/PlateView.swift index 36c30de..94506ae 100644 --- a/AutoCat/Views/PlateView.swift +++ b/AutoCat/Views/PlateView.swift @@ -14,8 +14,17 @@ class PlateView: UIView { private var countryLayer = CenterTextLayer() private var flagLayer = FlagLayer() - var number: PlateNumber? - var unrecognized: Bool = false + var number: PlateNumber? { + didSet { + self.layoutSubviews() + } + } + + var unrecognized: Bool = false { + didSet { + self.layoutSubviews() + } + } required init?(coder: NSCoder) { super.init(coder: coder) @@ -63,8 +72,6 @@ class PlateView: UIView { let fgColor = self.unrecognized ? /*fgColorErr*/UIColor.systemRed.cgColor : fgColorMain - print("Layout for number: \(number.asString())") - self.bgLayer.backgroundColor = fgColor self.bgLayer.frame = self.bounds