diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 88762ba..efb3956 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 6841A4F018B0E07966C1CEFC /* PagedResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6841A913FABBB0AB20DEF4FC /* PagedResponse.swift */; }; 6841A85D4B60DB71D1E68DA0 /* ImageGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6841AC687EA6293A0757678C /* ImageGrid.swift */; }; 7A000AA224C2EEDE001F5B00 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A000AA124C2EEDE001F5B00 /* Location.swift */; }; 7A0420AA25619AEC00034941 /* Osago.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0420A925619AEC00034941 /* Osago.swift */; }; @@ -81,6 +82,19 @@ 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 */; }; + 7AA7BC2C25A5DFB80053A5D5 /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11471C23FEA18700B424AF /* RxSwift */; }; + 7AA7BC2D25A5DFB80053A5D5 /* RxRelay in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11471E23FEA18700B424AF /* RxRelay */; }; + 7AA7BC2E25A5DFB80053A5D5 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472023FEA18700B424AF /* RxCocoa */; }; + 7AA7BC2F25A5DFB80053A5D5 /* RxBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472223FEA18700B424AF /* RxBlocking */; }; + 7AA7BC3025A5DFB80053A5D5 /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472523FEA1F400B424AF /* Realm */; }; + 7AA7BC3125A5DFB80053A5D5 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472723FEA1F400B424AF /* RealmSwift */; }; + 7AA7BC3225A5DFB80053A5D5 /* RxRealm in Frameworks */ = {isa = PBXBuildFile; productRef = 7A530B8A240181F500CBFE6E /* RxRealm */; }; + 7AA7BC3325A5DFB80053A5D5 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 7AF58D332402A91C00CE01A0 /* Kingfisher */; }; + 7AA7BC3425A5DFB80053A5D5 /* SwiftDate in Frameworks */ = {isa = PBXBuildFile; productRef = 7A051610241412CA00FC55AC /* SwiftDate */; }; + 7AA7BC3525A5DFB80053A5D5 /* ExceptionCatcher in Frameworks */ = {isa = PBXBuildFile; productRef = 7A813DC02508C4D900CC93B9 /* ExceptionCatcher */; }; + 7AA7BC3625A5DFB80053A5D5 /* PKHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABDE1C2532F3EB0041AFC6 /* PKHUD */; }; + 7AA7BC3725A5DFB80053A5D5 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABDE22253327F10041AFC6 /* DifferenceKit */; }; + 7AA7BC3825A5DFB80053A5D5 /* Eureka in Frameworks */ = {isa = PBXBuildFile; productRef = 7AEF47A3253DC4D2001D6238 /* Eureka */; }; 7AABDE26253350C30041AFC6 /* RxSectionedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */; }; 7AAE6AD324CDDF950023860B /* VehicleEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAE6AD224CDDF950023860B /* VehicleEvent.swift */; }; 7AB562BA249C9E9B00473D53 /* Region.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB562B9249C9E9B00473D53 /* Region.swift */; }; @@ -96,19 +110,6 @@ 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 */; }; - 7AE492922591FF5100322D2E /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11471C23FEA18700B424AF /* RxSwift */; }; - 7AE492932591FF5100322D2E /* RxRelay in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11471E23FEA18700B424AF /* RxRelay */; }; - 7AE492942591FF5100322D2E /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472023FEA18700B424AF /* RxCocoa */; }; - 7AE492952591FF5100322D2E /* RxBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472223FEA18700B424AF /* RxBlocking */; }; - 7AE492962591FF5100322D2E /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472523FEA1F400B424AF /* Realm */; }; - 7AE492972591FF5100322D2E /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472723FEA1F400B424AF /* RealmSwift */; }; - 7AE492982591FF5100322D2E /* RxRealm in Frameworks */ = {isa = PBXBuildFile; productRef = 7A530B8A240181F500CBFE6E /* RxRealm */; }; - 7AE492992591FF5100322D2E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 7AF58D332402A91C00CE01A0 /* Kingfisher */; }; - 7AE4929A2591FF5100322D2E /* SwiftDate in Frameworks */ = {isa = PBXBuildFile; productRef = 7A051610241412CA00FC55AC /* SwiftDate */; }; - 7AE4929B2591FF5100322D2E /* ExceptionCatcher in Frameworks */ = {isa = PBXBuildFile; productRef = 7A813DC02508C4D900CC93B9 /* ExceptionCatcher */; }; - 7AE4929C2591FF5100322D2E /* PKHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABDE1C2532F3EB0041AFC6 /* PKHUD */; }; - 7AE4929D2591FF5100322D2E /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABDE22253327F10041AFC6 /* DifferenceKit */; }; - 7AE4929E2591FF5100322D2E /* Eureka in Frameworks */ = {isa = PBXBuildFile; productRef = 7AEF47A3253DC4D2001D6238 /* Eureka */; }; 7AE492A1259232F000322D2E /* MultilineLinkRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE492A0259232F000322D2E /* MultilineLinkRow.swift */; }; 7AEFC3BE2529D3CC00BADFB2 /* ConfigurableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */; }; 7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFE727240455E200910EB7 /* SettingsController.swift */; }; @@ -116,6 +117,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 6841A913FABBB0AB20DEF4FC /* PagedResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagedResponse.swift; sourceTree = ""; }; 6841AC687EA6293A0757678C /* ImageGrid.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageGrid.swift; sourceTree = ""; }; 7A000AA124C2EEDE001F5B00 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; 7A0420A925619AEC00034941 /* Osago.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Osago.swift; sourceTree = ""; }; @@ -227,20 +229,20 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7AE492972591FF5100322D2E /* RealmSwift in Frameworks */, - 7AE4929B2591FF5100322D2E /* ExceptionCatcher in Frameworks */, - 7AE492962591FF5100322D2E /* Realm in Frameworks */, - 7AE492922591FF5100322D2E /* RxSwift in Frameworks */, - 7AE492952591FF5100322D2E /* RxBlocking in Frameworks */, - 7AE4929E2591FF5100322D2E /* Eureka in Frameworks */, - 7AE492992591FF5100322D2E /* Kingfisher in Frameworks */, + 7AA7BC3125A5DFB80053A5D5 /* RealmSwift in Frameworks */, + 7AA7BC3525A5DFB80053A5D5 /* ExceptionCatcher in Frameworks */, + 7AA7BC3025A5DFB80053A5D5 /* Realm in Frameworks */, + 7AA7BC2C25A5DFB80053A5D5 /* RxSwift in Frameworks */, + 7AA7BC2F25A5DFB80053A5D5 /* RxBlocking in Frameworks */, + 7AA7BC3825A5DFB80053A5D5 /* Eureka in Frameworks */, + 7AA7BC3325A5DFB80053A5D5 /* Kingfisher in Frameworks */, 7A813DBE2506A57100CC93B9 /* AuthenticationServices.framework in Frameworks */, - 7AE4929C2591FF5100322D2E /* PKHUD in Frameworks */, - 7AE492942591FF5100322D2E /* RxCocoa in Frameworks */, - 7AE4929A2591FF5100322D2E /* SwiftDate in Frameworks */, - 7AE4929D2591FF5100322D2E /* DifferenceKit in Frameworks */, - 7AE492982591FF5100322D2E /* RxRealm in Frameworks */, - 7AE492932591FF5100322D2E /* RxRelay in Frameworks */, + 7AA7BC3625A5DFB80053A5D5 /* PKHUD in Frameworks */, + 7AA7BC2E25A5DFB80053A5D5 /* RxCocoa in Frameworks */, + 7AA7BC3425A5DFB80053A5D5 /* SwiftDate in Frameworks */, + 7AA7BC3725A5DFB80053A5D5 /* DifferenceKit in Frameworks */, + 7AA7BC3225A5DFB80053A5D5 /* RxRealm in Frameworks */, + 7AA7BC2D25A5DFB80053A5D5 /* RxRelay in Frameworks */, 7A96AE2F246B2BCD00297C33 /* WebKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -298,7 +300,6 @@ isa = PBXGroup; children = ( 7A0420B825693CEE00034941 /* JS */, - 7A64AE6B2469DC6900ABE48E /* AutoCat.entitlements */, 7A3F07A924360D9100E59687 /* Extensions */, 7A6DD90424326788009DE740 /* Fonts */, 7A6DD901242BF48D009DE740 /* Views */, @@ -313,6 +314,7 @@ 7A11470923FDE7E600B424AF /* Assets.xcassets */, 7A11470B23FDE7E600B424AF /* LaunchScreen.storyboard */, 7A11470E23FDE7E600B424AF /* Info.plist */, + 7A64AE6B2469DC6900ABE48E /* AutoCat.entitlements */, 7A61FF892575A2CD00D905D5 /* Localizable.strings */, 7A61FFA2257D3CFC00D905D5 /* Localizable.stringsdict */, 7A61FF8F2575A5B300D905D5 /* InfoPlist.strings */, @@ -385,6 +387,7 @@ 7A2DE69725868AC800A113FC /* VehicleAd.swift */, 7AF12B1C258C9CFF0090F8B8 /* Cloneable.swift */, 7A8AB76725A0DC8200ECF2C1 /* DebugInfo.swift */, + 6841A913FABBB0AB20DEF4FC /* PagedResponse.swift */, ); path = Models; sourceTree = ""; @@ -666,6 +669,7 @@ 7A000AA224C2EEDE001F5B00 /* Location.swift in Sources */, 7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */, 6841A85D4B60DB71D1E68DA0 /* ImageGrid.swift in Sources */, + 6841A4F018B0E07966C1CEFC /* PagedResponse.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -843,7 +847,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 62; + CURRENT_PROJECT_VERSION = 63; DEVELOPMENT_TEAM = 46DTTB8X4S; INFOPLIST_FILE = AutoCat/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -866,7 +870,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 62; + CURRENT_PROJECT_VERSION = 63; DEVELOPMENT_TEAM = 46DTTB8X4S; INFOPLIST_FILE = AutoCat/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; diff --git a/AutoCat.xcodeproj/xcshareddata/xcschemes/AutoCat.xcscheme b/AutoCat.xcodeproj/xcshareddata/xcschemes/AutoCat.xcscheme index 462ef37..2c56fd0 100644 --- a/AutoCat.xcodeproj/xcshareddata/xcschemes/AutoCat.xcscheme +++ b/AutoCat.xcodeproj/xcshareddata/xcschemes/AutoCat.xcscheme @@ -51,6 +51,10 @@ ReferencedContainer = "container:AutoCat.xcodeproj"> + + ! + private let bag = DisposeBag() + private let searchController = UISearchController(searchResultsController: nil) + private var refreshControl = UIRefreshControl() + private var datasource: RxSectionedDataSource! + private var isLoadingPage = false + private var pageLoadingIndicator = UIActivityIndicatorView(style: .medium) var filterRelay = BehaviorRelay(value: Filter()) var filter = Filter() @@ -28,6 +30,8 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe navigationItem.searchController = searchController definesPresentationContext = true + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: self.pageLoadingIndicator) + //self.refreshControl.attributedTitle = NSAttributedString(string: "") self.refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged) self.tableView.addSubview(self.refreshControl) @@ -39,12 +43,17 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe self.filterRelay //.throttle(.seconds(2), scheduler: MainScheduler.instance) .debounce(.milliseconds(500), scheduler: MainScheduler.instance) - .flatMap { Api.getVehicles(with: $0).catchErrorJustReturn([]) } + .do(onNext: { _ in self.pageLoadingIndicator.startAnimating() }) + .flatMap { Api.getVehicles(with: $0, pageToken: self.datasource.pageToken).catchErrorJustReturn(PagedResponse()) } .observeOn(MainScheduler.instance) .do(onNext: { - self.navigationItem.title = String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""), $0.count) - self.showMapButton.isEnabled = $0.count > 0 + if let count = $0.count { + self.navigationItem.title = String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""), count) + self.showMapButton.isEnabled = count > 0 + } self.refreshControl.endRefreshing() + self.isLoadingPage = false + self.pageLoadingIndicator.stopAnimating() }) .bind(to: self.datasource.data) .disposed(by: self.bag) @@ -163,4 +172,14 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe let vehicle = self.datasource.item(at: indexPath) self.updateDetailController(with: vehicle) } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard tableView.contentSize.height > 0 else { return } + + let toBottom = tableView.contentSize.height - (tableView.contentOffset.y + tableView.frame.size.height) + if toBottom < 100 && !self.isLoadingPage && self.datasource.needMoreData() { + self.isLoadingPage = true + self.filterRelay.accept(self.filter) + } + } } diff --git a/AutoCat/Models/PagedResponse.swift b/AutoCat/Models/PagedResponse.swift new file mode 100644 index 0000000..a6021c3 --- /dev/null +++ b/AutoCat/Models/PagedResponse.swift @@ -0,0 +1,13 @@ +import Foundation + +class PagedResponse: Decodable where T: Decodable { + let count: Int? + let pageToken: String? + let items: [T] + + init() { + self.items = [] + self.count = nil + self.pageToken = nil + } +} diff --git a/AutoCat/Models/Vehicle.swift b/AutoCat/Models/Vehicle.swift index 496e1eb..eff6aa7 100644 --- a/AutoCat/Models/Vehicle.swift +++ b/AutoCat/Models/Vehicle.swift @@ -207,6 +207,7 @@ class Vehicle: Object, Decodable, Identifiable, Differentiable, Cloneable { case isRightWheel case isJapanese case addedDate + case updatedDate case addedBy case photos case ownershipPeriods @@ -235,6 +236,7 @@ class Vehicle: Object, Decodable, Identifiable, Differentiable, Cloneable { self.addedDate = (try container.decode(TimeInterval.self, forKey: .addedDate))/1000 self.addedBy = try container.decode(String.self, forKey: .addedBy) self.debugInfo = try container.decodeIfPresent(DebugInfo.self, forKey: .debugInfo) + self.updatedDate = (try container.decode(TimeInterval.self, forKey: .updatedDate))/1000 if let photosArray = try container.decodeIfPresent([VehiclePhoto].self, forKey: .photos) { self.photos.append(objectsIn: photosArray) @@ -248,12 +250,6 @@ class Vehicle: Object, Decodable, Identifiable, Differentiable, Cloneable { 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 - } - if let osago = try container.decodeIfPresent([Osago].self, forKey: .osagoContracts) { self.osagoContracts.append(objectsIn: osago) } diff --git a/AutoCat/Utils/Api.swift b/AutoCat/Utils/Api.swift index b0ec4fa..bdb5df5 100644 --- a/AutoCat/Utils/Api.swift +++ b/AutoCat/Utils/Api.swift @@ -194,8 +194,12 @@ class Api { return self.makeBodyRequest(api: "user/signup", body: body) } - public static func getVehicles(with filter: Filter) -> Single<[Vehicle]> { - return self.makeGetRequest(api: "vehicles", params: filter.queryDictionary()) + public static func getVehicles(with filter: Filter, pageToken: String? = nil) -> Single> { + var params = filter.queryDictionary() + if let token = pageToken { + params["pageToken"] = token; + } + return self.makeGetRequest(api: "vehicles", params: params) } public static func checkVehicle(by number: String, force: Bool = false) -> Single { diff --git a/AutoCat/Utils/RxSectionedDataSource.swift b/AutoCat/Utils/RxSectionedDataSource.swift index 851a274..ff823d4 100644 --- a/AutoCat/Utils/RxSectionedDataSource.swift +++ b/AutoCat/Utils/RxSectionedDataSource.swift @@ -3,10 +3,13 @@ import DifferenceKit import RxSwift import RxCocoa -class RxSectionedDataSource: NSObject, UITableViewDataSource where Item: Dated & Hashable & Differentiable, Cell: UITableViewCell & ConfigurableCell, Cell.Item == Item { +class RxSectionedDataSource: NSObject, UITableViewDataSource where Item: Dated & Hashable & Differentiable & Decodable, Cell: UITableViewCell & ConfigurableCell, Cell.Item == Item { private var tv: UITableView private var cellIdentifier: String private var sections: [DateSection] = [] + private var items: [Item] = [] + private(set) var pageToken: String? = nil + private(set) var count: Int? = nil init(table: UITableView, cellIdentifier: String = String(describing: Cell.self)) { self.tv = table @@ -49,13 +52,26 @@ class RxSectionedDataSource: NSObject, UITableViewDataSource where I self.sections[indexPath.section].elements[indexPath.row] = item } - var data: Binder<[Item]> { + var data: Binder> { return Binder(self) { datasource, data in - let newSections = data.groupedByDate() + if let count = data.count { + self.count = count + self.items = data.items + } else { + self.items.append(contentsOf: data.items) + } + self.pageToken = data.pageToken + + let newSections = self.items.groupedByDate() let changeset = StagedChangeset(source: self.sections, target: newSections) self.tv.reload(using: changeset, with: .automatic) { newSects in self.sections = newSects } } } + + func needMoreData() -> Bool { + guard let count = self.count else { return true } + return self.items.count < count + } }