From 64cb79360fbfd29ff0663f2f4c2fdcc650e88e8a Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Mon, 24 Feb 2020 01:30:42 +0300 Subject: [PATCH] Initial commit --- .gitignore | 1 + AutoCat.xcodeproj/project.pbxproj | 340 +++++++++++++- .../contents.xcworkspacedata | 2 +- .../xcshareddata/swiftpm/Package.resolved | 79 ++++ .../xcdebugger/Breakpoints_v2.xcbkptlist | 44 ++ AutoCat/AppDelegate.swift | 14 +- AutoCat/Base.lproj/Main.storyboard | 434 +++++++++++++++++- AutoCat/Cells/SectionHeader.swift | 12 + AutoCat/Cells/SectionHeader.xib | 48 ++ AutoCat/Cells/VehicleCell.swift | 18 + AutoCat/Cells/VehicleHeaderCell.swift | 20 + AutoCat/Cells/VehiclePhotoCell.swift | 22 + AutoCat/Cells/VehicleTextParamCell.swift | 18 + AutoCat/Controllers/AuthController.swift | 58 +++ AutoCat/Controllers/CheckController.swift | 91 ++++ AutoCat/Controllers/MainSplitController.swift | 58 +++ AutoCat/Controllers/ReportController.swift | 217 +++++++++ AutoCat/Info.plist | 2 - AutoCat/Models/Response.swift | 30 ++ AutoCat/Models/Settings.swift | 24 + AutoCat/Models/User.swift | 11 + AutoCat/Models/Vehicle.swift | 51 ++ AutoCat/SceneDelegate.swift | 22 +- .../Reactive+RxRealmDataSources.swift | 113 +++++ .../RealmBindObserver.swift | 41 ++ .../RxCollectionViewRealmDataSource.swift | 210 +++++++++ .../RxTableViewRealmDataSource.swift | 223 +++++++++ AutoCat/Utils/Api.swift | 69 +++ AutoCat/ViewController.swift | 20 - Cartfile | 1 + Cartfile.resolved | 1 + input.xcfilelist | 1 + output.xcfilelist | 1 + 33 files changed, 2238 insertions(+), 58 deletions(-) create mode 100644 .gitignore create mode 100644 AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 AutoCat/Cells/SectionHeader.swift create mode 100644 AutoCat/Cells/SectionHeader.xib create mode 100644 AutoCat/Cells/VehicleCell.swift create mode 100644 AutoCat/Cells/VehicleHeaderCell.swift create mode 100644 AutoCat/Cells/VehiclePhotoCell.swift create mode 100644 AutoCat/Cells/VehicleTextParamCell.swift create mode 100644 AutoCat/Controllers/AuthController.swift create mode 100644 AutoCat/Controllers/CheckController.swift create mode 100644 AutoCat/Controllers/MainSplitController.swift create mode 100644 AutoCat/Controllers/ReportController.swift create mode 100644 AutoCat/Models/Response.swift create mode 100644 AutoCat/Models/Settings.swift create mode 100644 AutoCat/Models/User.swift create mode 100644 AutoCat/Models/Vehicle.swift create mode 100644 AutoCat/ThirdParty/RxRealmDataSources/Reactive+RxRealmDataSources.swift create mode 100644 AutoCat/ThirdParty/RxRealmDataSources/RealmBindObserver.swift create mode 100644 AutoCat/ThirdParty/RxRealmDataSources/RxCollectionViewRealmDataSource.swift create mode 100644 AutoCat/ThirdParty/RxRealmDataSources/RxTableViewRealmDataSource.swift create mode 100644 AutoCat/Utils/Api.swift delete mode 100644 AutoCat/ViewController.swift create mode 100644 Cartfile create mode 100644 Cartfile.resolved create mode 100644 input.xcfilelist create mode 100644 output.xcfilelist diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25c49f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +Carthage/ diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 73a4689..59dc222 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -3,27 +3,76 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ 7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11470023FDE7E500B424AF /* AppDelegate.swift */; }; 7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11470223FDE7E500B424AF /* SceneDelegate.swift */; }; - 7A11470523FDE7E500B424AF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11470423FDE7E500B424AF /* ViewController.swift */; }; 7A11470823FDE7E500B424AF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7A11470623FDE7E500B424AF /* Main.storyboard */; }; 7A11470A23FDE7E600B424AF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A11470923FDE7E600B424AF /* Assets.xcassets */; }; 7A11470D23FDE7E600B424AF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7A11470B23FDE7E600B424AF /* LaunchScreen.storyboard */; }; + 7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11471523FDEB2A00B424AF /* MainSplitController.swift */; }; + 7A11471823FDEBFA00B424AF /* ReportController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11471723FDEBFA00B424AF /* ReportController.swift */; }; + 7A11471A23FE839000B424AF /* AuthController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11471923FE839000B424AF /* AuthController.swift */; }; + 7A11471D23FEA18700B424AF /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11471C23FEA18700B424AF /* RxSwift */; }; + 7A11471F23FEA18700B424AF /* RxRelay in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11471E23FEA18700B424AF /* RxRelay */; }; + 7A11472123FEA18700B424AF /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472023FEA18700B424AF /* RxCocoa */; }; + 7A11472323FEA18700B424AF /* RxBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472223FEA18700B424AF /* RxBlocking */; }; + 7A11472623FEA1F400B424AF /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472523FEA1F400B424AF /* Realm */; }; + 7A11472823FEA1F400B424AF /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472723FEA1F400B424AF /* RealmSwift */; }; + 7A11472B23FEA24D00B424AF /* Action in Frameworks */ = {isa = PBXBuildFile; productRef = 7A11472A23FEA24D00B424AF /* Action */; }; + 7A11474423FF06CA00B424AF /* Api.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474323FF06CA00B424AF /* Api.swift */; }; + 7A11474723FF2AA500B424AF /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474623FF2AA500B424AF /* User.swift */; }; + 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 */; }; + 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 */; }; + 7A530B802401803A00CBFE6E /* Vehicle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7F2401803A00CBFE6E /* Vehicle.swift */; }; + 7A530B85240180CC00CBFE6E /* RxCollectionViewRealmDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B81240180CC00CBFE6E /* RxCollectionViewRealmDataSource.swift */; }; + 7A530B86240180CC00CBFE6E /* RealmBindObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B82240180CC00CBFE6E /* RealmBindObserver.swift */; }; + 7A530B87240180CC00CBFE6E /* Reactive+RxRealmDataSources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B83240180CC00CBFE6E /* Reactive+RxRealmDataSources.swift */; }; + 7A530B88240180CC00CBFE6E /* RxTableViewRealmDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B84240180CC00CBFE6E /* RxTableViewRealmDataSource.swift */; }; + 7A530B8B240181F500CBFE6E /* RxRealm in Frameworks */ = {isa = PBXBuildFile; productRef = 7A530B8A240181F500CBFE6E /* RxRealm */; }; + 7A7547DD2403180A004E8406 /* SectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7547DB2403180A004E8406 /* SectionHeader.swift */; }; + 7A7547DE2403180A004E8406 /* SectionHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7A7547DC2403180A004E8406 /* SectionHeader.xib */; }; + 7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */; }; + 7AF58D2F24029C5200CE01A0 /* MagazineLayout in Frameworks */ = {isa = PBXBuildFile; productRef = 7AF58D2E24029C5200CE01A0 /* MagazineLayout */; }; + 7AF58D3124029E1000CE01A0 /* VehicleHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF58D3024029E1000CE01A0 /* VehicleHeaderCell.swift */; }; + 7AF58D342402A91C00CE01A0 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 7AF58D332402A91C00CE01A0 /* Kingfisher */; }; + 7AF58D58240309CA00CE01A0 /* VehicleTextParamCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF58D57240309CA00CE01A0 /* VehicleTextParamCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 7A1146FD23FDE7E500B424AF /* AutoCat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AutoCat.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7A11470023FDE7E500B424AF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7A11470223FDE7E500B424AF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 7A11470423FDE7E500B424AF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 7A11470723FDE7E500B424AF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 7A11470923FDE7E600B424AF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 7A11470C23FDE7E600B424AF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 7A11470E23FDE7E600B424AF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7A11471523FDEB2A00B424AF /* MainSplitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitController.swift; sourceTree = ""; }; + 7A11471723FDEBFA00B424AF /* ReportController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportController.swift; sourceTree = ""; }; + 7A11471923FE839000B424AF /* AuthController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthController.swift; sourceTree = ""; }; + 7A11474323FF06CA00B424AF /* Api.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Api.swift; sourceTree = ""; }; + 7A11474623FF2AA500B424AF /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + 7A530B81240180CC00CBFE6E /* RxCollectionViewRealmDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxCollectionViewRealmDataSource.swift; sourceTree = ""; }; + 7A530B82240180CC00CBFE6E /* RealmBindObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmBindObserver.swift; sourceTree = ""; }; + 7A530B83240180CC00CBFE6E /* Reactive+RxRealmDataSources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Reactive+RxRealmDataSources.swift"; sourceTree = ""; }; + 7A530B84240180CC00CBFE6E /* RxTableViewRealmDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxTableViewRealmDataSource.swift; sourceTree = ""; }; + 7A7547DB2403180A004E8406 /* SectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeader.swift; sourceTree = ""; }; + 7A7547DC2403180A004E8406 /* SectionHeader.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SectionHeader.xib; sourceTree = ""; }; + 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclePhotoCell.swift; sourceTree = ""; }; + 7AF58D3024029E1000CE01A0 /* VehicleHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleHeaderCell.swift; sourceTree = ""; }; + 7AF58D57240309CA00CE01A0 /* VehicleTextParamCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleTextParamCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -31,6 +80,18 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7AF58D342402A91C00CE01A0 /* Kingfisher in Frameworks */, + 7A11472823FEA1F400B424AF /* RealmSwift in Frameworks */, + 7A11472123FEA18700B424AF /* RxCocoa in Frameworks */, + 7A11474E23FFEE8800B424AF /* SVProgressHUD.framework in Frameworks */, + 7A11472323FEA18700B424AF /* RxBlocking in Frameworks */, + 7A530B8B240181F500CBFE6E /* RxRealm in Frameworks */, + 7A11472B23FEA24D00B424AF /* Action in Frameworks */, + 7A11471F23FEA18700B424AF /* RxRelay in Frameworks */, + 7AF58D2F24029C5200CE01A0 /* MagazineLayout in Frameworks */, + 7A530B78240010D900CBFE6E /* InputMask in Frameworks */, + 7A11471D23FEA18700B424AF /* RxSwift in Frameworks */, + 7A11472623FEA1F400B424AF /* Realm in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -42,6 +103,7 @@ children = ( 7A1146FF23FDE7E500B424AF /* AutoCat */, 7A1146FE23FDE7E500B424AF /* Products */, + 7A11474C23FFEE8700B424AF /* Frameworks */, ); sourceTree = ""; }; @@ -56,9 +118,13 @@ 7A1146FF23FDE7E500B424AF /* AutoCat */ = { isa = PBXGroup; children = ( + 7A530B7C24017FBE00CBFE6E /* Cells */, + 7A11474523FF2A9000B424AF /* Models */, + 7A11474223FF06B600B424AF /* Utils */, + 7A11472C23FECA3E00B424AF /* ThirdParty */, + 7A11471423FDEAF800B424AF /* Controllers */, 7A11470023FDE7E500B424AF /* AppDelegate.swift */, 7A11470223FDE7E500B424AF /* SceneDelegate.swift */, - 7A11470423FDE7E500B424AF /* ViewController.swift */, 7A11470623FDE7E500B424AF /* Main.storyboard */, 7A11470923FDE7E600B424AF /* Assets.xcassets */, 7A11470B23FDE7E600B424AF /* LaunchScreen.storyboard */, @@ -67,6 +133,76 @@ path = AutoCat; sourceTree = ""; }; + 7A11471423FDEAF800B424AF /* Controllers */ = { + isa = PBXGroup; + children = ( + 7A11471523FDEB2A00B424AF /* MainSplitController.swift */, + 7A11471723FDEBFA00B424AF /* ReportController.swift */, + 7A11471923FE839000B424AF /* AuthController.swift */, + 7A530B7924001D3300CBFE6E /* CheckController.swift */, + ); + path = Controllers; + sourceTree = ""; + }; + 7A11472C23FECA3E00B424AF /* ThirdParty */ = { + isa = PBXGroup; + children = ( + 7A530B7B24017DED00CBFE6E /* RxRealmDataSources */, + ); + path = ThirdParty; + sourceTree = ""; + }; + 7A11474223FF06B600B424AF /* Utils */ = { + isa = PBXGroup; + children = ( + 7A11474323FF06CA00B424AF /* Api.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 7A11474523FF2A9000B424AF /* Models */ = { + isa = PBXGroup; + children = ( + 7A11474623FF2AA500B424AF /* User.swift */, + 7A11474823FF2B2D00B424AF /* Response.swift */, + 7A11474A23FF368B00B424AF /* Settings.swift */, + 7A530B7F2401803A00CBFE6E /* Vehicle.swift */, + ); + path = Models; + sourceTree = ""; + }; + 7A11474C23FFEE8700B424AF /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7A11474D23FFEE8800B424AF /* SVProgressHUD.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 7A530B7B24017DED00CBFE6E /* RxRealmDataSources */ = { + isa = PBXGroup; + children = ( + 7A530B83240180CC00CBFE6E /* Reactive+RxRealmDataSources.swift */, + 7A530B82240180CC00CBFE6E /* RealmBindObserver.swift */, + 7A530B81240180CC00CBFE6E /* RxCollectionViewRealmDataSource.swift */, + 7A530B84240180CC00CBFE6E /* RxTableViewRealmDataSource.swift */, + ); + path = RxRealmDataSources; + sourceTree = ""; + }; + 7A530B7C24017FBE00CBFE6E /* Cells */ = { + isa = PBXGroup; + children = ( + 7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */, + 7AF58D3024029E1000CE01A0 /* VehicleHeaderCell.swift */, + 7AF58D57240309CA00CE01A0 /* VehicleTextParamCell.swift */, + 7A7547DB2403180A004E8406 /* SectionHeader.swift */, + 7A7547DC2403180A004E8406 /* SectionHeader.xib */, + 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -77,12 +213,26 @@ 7A1146F923FDE7E500B424AF /* Sources */, 7A1146FA23FDE7E500B424AF /* Frameworks */, 7A1146FB23FDE7E500B424AF /* Resources */, + 7A11475123FFF02E00B424AF /* ShellScript */, ); buildRules = ( ); dependencies = ( ); name = AutoCat; + packageProductDependencies = ( + 7A11471C23FEA18700B424AF /* RxSwift */, + 7A11471E23FEA18700B424AF /* RxRelay */, + 7A11472023FEA18700B424AF /* RxCocoa */, + 7A11472223FEA18700B424AF /* RxBlocking */, + 7A11472523FEA1F400B424AF /* Realm */, + 7A11472723FEA1F400B424AF /* RealmSwift */, + 7A11472A23FEA24D00B424AF /* Action */, + 7A530B77240010D900CBFE6E /* InputMask */, + 7A530B8A240181F500CBFE6E /* RxRealm */, + 7AF58D2E24029C5200CE01A0 /* MagazineLayout */, + 7AF58D332402A91C00CE01A0 /* Kingfisher */, + ); productName = AutoCat; productReference = 7A1146FD23FDE7E500B424AF /* AutoCat.app */; productType = "com.apple.product-type.application"; @@ -111,6 +261,15 @@ Base, ); mainGroup = 7A1146F423FDE7E500B424AF; + packageReferences = ( + 7A11471B23FEA18700B424AF /* XCRemoteSwiftPackageReference "RxSwift" */, + 7A11472423FEA1F400B424AF /* XCRemoteSwiftPackageReference "realm-cocoa" */, + 7A11472923FEA24D00B424AF /* XCRemoteSwiftPackageReference "Action" */, + 7A530B76240010D900CBFE6E /* XCRemoteSwiftPackageReference "input-mask-ios" */, + 7A530B89240181F500CBFE6E /* XCRemoteSwiftPackageReference "RxRealm" */, + 7AF58D2D24029C5200CE01A0 /* XCRemoteSwiftPackageReference "MagazineLayout" */, + 7AF58D322402A91C00CE01A0 /* XCRemoteSwiftPackageReference "Kingfisher" */, + ); productRefGroup = 7A1146FE23FDE7E500B424AF /* Products */; projectDirPath = ""; projectRoot = ""; @@ -125,6 +284,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7A7547DE2403180A004E8406 /* SectionHeader.xib in Resources */, 7A11470D23FDE7E600B424AF /* LaunchScreen.storyboard in Resources */, 7A11470A23FDE7E600B424AF /* Assets.xcassets in Resources */, 7A11470823FDE7E500B424AF /* Main.storyboard in Resources */, @@ -133,14 +293,53 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 7A11475123FFF02E00B424AF /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "$(SRCROOT)/input.xcfilelist", + ); + inputPaths = ( + ); + outputFileListPaths = ( + "$(SRCROOT)/output.xcfilelist", + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n/usr/local/bin/carthage copy-frameworks\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 7A1146F923FDE7E500B424AF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7A11470523FDE7E500B424AF /* ViewController.swift in Sources */, + 7A530B802401803A00CBFE6E /* Vehicle.swift in Sources */, 7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */, + 7A530B86240180CC00CBFE6E /* RealmBindObserver.swift in Sources */, + 7A11474923FF2B2D00B424AF /* Response.swift in Sources */, + 7A11471823FDEBFA00B424AF /* ReportController.swift in Sources */, + 7A11471A23FE839000B424AF /* AuthController.swift in Sources */, + 7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */, + 7A7547DD2403180A004E8406 /* SectionHeader.swift in Sources */, + 7AF58D58240309CA00CE01A0 /* VehicleTextParamCell.swift in Sources */, + 7A11474723FF2AA500B424AF /* User.swift in Sources */, + 7A530B88240180CC00CBFE6E /* RxTableViewRealmDataSource.swift in Sources */, + 7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */, + 7AF58D3124029E1000CE01A0 /* VehicleHeaderCell.swift in Sources */, + 7A530B85240180CC00CBFE6E /* RxCollectionViewRealmDataSource.swift in Sources */, 7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */, + 7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */, + 7A530B87240180CC00CBFE6E /* Reactive+RxRealmDataSources.swift in Sources */, + 7A11474423FF06CA00B424AF /* Api.swift in Sources */, + 7A11474B23FF368B00B424AF /* Settings.swift in Sources */, + 7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -286,12 +485,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 46DTTB8X4S; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); INFOPLIST_FILE = AutoCat/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCat.AutoCat; + PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCat; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -304,12 +508,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 46DTTB8X4S; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); INFOPLIST_FILE = AutoCat/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCat.AutoCat; + PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCat; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -338,6 +547,123 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 7A11471B23FEA18700B424AF /* XCRemoteSwiftPackageReference "RxSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ReactiveX/RxSwift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.0.1; + }; + }; + 7A11472423FEA1F400B424AF /* XCRemoteSwiftPackageReference "realm-cocoa" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/realm/realm-cocoa"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.3.2; + }; + }; + 7A11472923FEA24D00B424AF /* XCRemoteSwiftPackageReference "Action" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/RxSwiftCommunity/Action"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.0.1; + }; + }; + 7A530B76240010D900CBFE6E /* XCRemoteSwiftPackageReference "input-mask-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/RedMadRobot/input-mask-ios"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.0.0; + }; + }; + 7A530B89240181F500CBFE6E /* XCRemoteSwiftPackageReference "RxRealm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/RxSwiftCommunity/RxRealm"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; + 7AF58D2D24029C5200CE01A0 /* XCRemoteSwiftPackageReference "MagazineLayout" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/airbnb/MagazineLayout"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.5.5; + }; + }; + 7AF58D322402A91C00CE01A0 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.13.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 7A11471C23FEA18700B424AF /* RxSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 7A11471B23FEA18700B424AF /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxSwift; + }; + 7A11471E23FEA18700B424AF /* RxRelay */ = { + isa = XCSwiftPackageProductDependency; + package = 7A11471B23FEA18700B424AF /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxRelay; + }; + 7A11472023FEA18700B424AF /* RxCocoa */ = { + isa = XCSwiftPackageProductDependency; + package = 7A11471B23FEA18700B424AF /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxCocoa; + }; + 7A11472223FEA18700B424AF /* RxBlocking */ = { + isa = XCSwiftPackageProductDependency; + package = 7A11471B23FEA18700B424AF /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxBlocking; + }; + 7A11472523FEA1F400B424AF /* Realm */ = { + isa = XCSwiftPackageProductDependency; + package = 7A11472423FEA1F400B424AF /* XCRemoteSwiftPackageReference "realm-cocoa" */; + productName = Realm; + }; + 7A11472723FEA1F400B424AF /* RealmSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 7A11472423FEA1F400B424AF /* XCRemoteSwiftPackageReference "realm-cocoa" */; + productName = RealmSwift; + }; + 7A11472A23FEA24D00B424AF /* Action */ = { + isa = XCSwiftPackageProductDependency; + package = 7A11472923FEA24D00B424AF /* XCRemoteSwiftPackageReference "Action" */; + productName = Action; + }; + 7A530B77240010D900CBFE6E /* InputMask */ = { + isa = XCSwiftPackageProductDependency; + package = 7A530B76240010D900CBFE6E /* XCRemoteSwiftPackageReference "input-mask-ios" */; + productName = InputMask; + }; + 7A530B8A240181F500CBFE6E /* RxRealm */ = { + isa = XCSwiftPackageProductDependency; + package = 7A530B89240181F500CBFE6E /* XCRemoteSwiftPackageReference "RxRealm" */; + productName = RxRealm; + }; + 7AF58D2E24029C5200CE01A0 /* MagazineLayout */ = { + isa = XCSwiftPackageProductDependency; + package = 7AF58D2D24029C5200CE01A0 /* XCRemoteSwiftPackageReference "MagazineLayout" */; + productName = MagazineLayout; + }; + 7AF58D332402A91C00CE01A0 /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 7AF58D322402A91C00CE01A0 /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 7A1146F523FDE7E500B424AF /* Project object */; } diff --git a/AutoCat.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/AutoCat.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 55eedc2..919434a 100644 --- a/AutoCat.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/AutoCat.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..7b73d97 --- /dev/null +++ b/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,79 @@ +{ + "object": { + "pins": [ + { + "package": "Action", + "repositoryURL": "https://github.com/RxSwiftCommunity/Action", + "state": { + "branch": null, + "revision": "cdade63f7bbe1f5e1eff7779e5858a796dc2c001", + "version": "4.0.1" + } + }, + { + "package": "InputMask", + "repositoryURL": "https://github.com/RedMadRobot/input-mask-ios", + "state": { + "branch": null, + "revision": "78a98f31d5b7fe2c0d84146748f22114c1c17c7c", + "version": "5.0.0" + } + }, + { + "package": "Kingfisher", + "repositoryURL": "https://github.com/onevcat/Kingfisher", + "state": { + "branch": null, + "revision": "46bf251fee8ed426921c647790f08ca8ad0105a9", + "version": "5.13.1" + } + }, + { + "package": "MagazineLayout", + "repositoryURL": "https://github.com/airbnb/MagazineLayout", + "state": { + "branch": null, + "revision": "bbbe1456c34c1abb527d05ff9da3ff2a54584d78", + "version": "1.5.5" + } + }, + { + "package": "Realm", + "repositoryURL": "https://github.com/realm/realm-cocoa", + "state": { + "branch": null, + "revision": "913d59c6522e3007c7cbac3a8e9775abf72c054c", + "version": "4.3.2" + } + }, + { + "package": "RealmCore", + "repositoryURL": "https://github.com/realm/realm-core", + "state": { + "branch": null, + "revision": "35662ff940e340bf630ad1d1d88acfc7af18bee6", + "version": "5.23.8" + } + }, + { + "package": "RxRealm", + "repositoryURL": "https://github.com/RxSwiftCommunity/RxRealm", + "state": { + "branch": null, + "revision": "70188d79fe2eb19b5013dd1deae33e9e53f10e76", + "version": "2.0.0" + } + }, + { + "package": "RxSwift", + "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", + "state": { + "branch": null, + "revision": "b3e888b4972d9bc76495dd74d30a8c7fad4b9395", + "version": "5.0.1" + } + } + ] + }, + "version": 1 +} diff --git a/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..86a6745 --- /dev/null +++ b/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + diff --git a/AutoCat/AppDelegate.swift b/AutoCat/AppDelegate.swift index 6c61a64..5d00cfa 100644 --- a/AutoCat/AppDelegate.swift +++ b/AutoCat/AppDelegate.swift @@ -1,12 +1,5 @@ -// -// AppDelegate.swift -// AutoCat -// -// Created by Selim Mustafaev on 20.02.2020. -// Copyright © 2020 Selim Mustafaev. All rights reserved. -// - import UIKit +import SVProgressHUD @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -14,7 +7,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + + SVProgressHUD.setDefaultStyle(.dark) + SVProgressHUD.setDefaultMaskType(.black) + return true } diff --git a/AutoCat/Base.lproj/Main.storyboard b/AutoCat/Base.lproj/Main.storyboard index 25a7638..30198de 100644 --- a/AutoCat/Base.lproj/Main.storyboard +++ b/AutoCat/Base.lproj/Main.storyboard @@ -1,24 +1,440 @@ - + + - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + diff --git a/AutoCat/Cells/SectionHeader.swift b/AutoCat/Cells/SectionHeader.swift new file mode 100644 index 0000000..82bf5e7 --- /dev/null +++ b/AutoCat/Cells/SectionHeader.swift @@ -0,0 +1,12 @@ +import UIKit +import MagazineLayout + +class SectionHeader: MagazineLayoutCollectionReusableView { + @IBOutlet weak var title: UILabel! + @IBOutlet weak var dividerHeightConstraint: NSLayoutConstraint! + + override func awakeFromNib() { + super.awakeFromNib() + self.dividerHeightConstraint.constant = 1/UIScreen.main.scale + } +} diff --git a/AutoCat/Cells/SectionHeader.xib b/AutoCat/Cells/SectionHeader.xib new file mode 100644 index 0000000..155abe0 --- /dev/null +++ b/AutoCat/Cells/SectionHeader.xib @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AutoCat/Cells/VehicleCell.swift b/AutoCat/Cells/VehicleCell.swift new file mode 100644 index 0000000..1a5ea88 --- /dev/null +++ b/AutoCat/Cells/VehicleCell.swift @@ -0,0 +1,18 @@ +import UIKit + +class VehicleCell: UITableViewCell { + + @IBOutlet weak var name: UILabel! + @IBOutlet weak var number: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + func configure(with vehicle: Vehicle) { + self.name.text = vehicle.brand?.name?.original + self.number.text = vehicle.number + } + +} diff --git a/AutoCat/Cells/VehicleHeaderCell.swift b/AutoCat/Cells/VehicleHeaderCell.swift new file mode 100644 index 0000000..77afc4e --- /dev/null +++ b/AutoCat/Cells/VehicleHeaderCell.swift @@ -0,0 +1,20 @@ +import UIKit +import MagazineLayout +import Kingfisher + +class VehicleHeaderCell: MagazineLayoutCollectionViewCell { + @IBOutlet weak var logo: UIImageView! + @IBOutlet weak var name: UILabel! + + override func prepareForReuse() { + self.logo.kf.cancelDownloadTask() + } + + func configure(with vehicle: Vehicle) { + self.name.text = vehicle.brand?.name?.original + + if let url = vehicle.brand?.logo { + self.logo.kf.setImage(with: URL(string: url)) + } + } +} diff --git a/AutoCat/Cells/VehiclePhotoCell.swift b/AutoCat/Cells/VehiclePhotoCell.swift new file mode 100644 index 0000000..a6fe491 --- /dev/null +++ b/AutoCat/Cells/VehiclePhotoCell.swift @@ -0,0 +1,22 @@ +import UIKit +import MagazineLayout +import Kingfisher + +class VehiclePhotoCell: MagazineLayoutCollectionViewCell { + @IBOutlet weak var photo: UIImageView! + @IBOutlet weak var model: UILabel! + @IBOutlet weak var date: UILabel! + + override func prepareForReuse() { + super.prepareForReuse() + self.photo.kf.cancelDownloadTask() + } + + func configure(with photoModel: VehiclePhoto) { + if let url = URL(string: photoModel.url) { + self.photo.kf.setImage(with: url) + } + + self.model.text = "\(photoModel.brand ?? "") \(photoModel.model ?? "")" + } +} diff --git a/AutoCat/Cells/VehicleTextParamCell.swift b/AutoCat/Cells/VehicleTextParamCell.swift new file mode 100644 index 0000000..b0fe4d1 --- /dev/null +++ b/AutoCat/Cells/VehicleTextParamCell.swift @@ -0,0 +1,18 @@ +import UIKit +import MagazineLayout + +class VehicleTextParamCell: MagazineLayoutCollectionViewCell { + @IBOutlet weak var paramName: UILabel! + @IBOutlet weak var paramValue: UILabel! + @IBOutlet weak var dividerHeightConstraint: NSLayoutConstraint! + + override func awakeFromNib() { + super.awakeFromNib() + self.dividerHeightConstraint.constant = 1/UIScreen.main.scale + } + + func configure(param: String, value: String) { + self.paramName.text = param + self.paramValue.text = value + } +} diff --git a/AutoCat/Controllers/AuthController.swift b/AutoCat/Controllers/AuthController.swift new file mode 100644 index 0000000..0e1eabe --- /dev/null +++ b/AutoCat/Controllers/AuthController.swift @@ -0,0 +1,58 @@ +import UIKit +import RxSwift +import RxCocoa +import SVProgressHUD + +class AuthController: UIViewController { + + @IBOutlet weak var username: UITextField! + @IBOutlet weak var password: UITextField! + @IBOutlet weak var login: UIButton! + @IBOutlet weak var signup: UIButton! + + let bag = DisposeBag() + + override func viewDidLoad() { + super.viewDidLoad() + + let authValid = Observable.combineLatest(self.username.rx.text, self.password.rx.text) { name, pass -> Bool in + guard let name = name, let pass = pass else { return false } + return name.count >= 4 && pass.count >= 5 + } + + authValid.bind(to: self.login.rx.isEnabled).disposed(by: self.bag) + authValid.bind(to: self.signup.rx.isEnabled).disposed(by: self.bag) + } + + @IBAction func loginTapped(_ sender: UIButton) { + guard let name = self.username.text, let pass = self.password.text else { return } + + SVProgressHUD.show() + Api.login(username: name, password: pass) + .observeOn(MainScheduler.instance) + .subscribe(onNext: self.goToMainScreen(user:), onError: self.displayError(error:)) + .disposed(by: self.bag) + } + + @IBAction func signupTapped(_ sender: UIButton) { + guard let name = self.username.text, let pass = self.password.text else { return } + + SVProgressHUD.show() + Api.signup(username: name, password: pass) + .observeOn(MainScheduler.instance) + .subscribe(onNext: self.goToMainScreen(user:), onError: self.displayError(error:)) + .disposed(by: self.bag) + } + + func goToMainScreen(user: User) { + SVProgressHUD.dismiss() + Settings.shared.user = user + let storyboard = UIStoryboard(name: "Main", bundle: nil) + self.view.window?.rootViewController = storyboard.instantiateViewController(identifier: "MainSplitController") + } + + func displayError(error: Error) { + SVProgressHUD.showError(withStatus: error.localizedDescription) + print(error) + } +} diff --git a/AutoCat/Controllers/CheckController.swift b/AutoCat/Controllers/CheckController.swift new file mode 100644 index 0000000..f84e0fb --- /dev/null +++ b/AutoCat/Controllers/CheckController.swift @@ -0,0 +1,91 @@ +import UIKit +import InputMask +import RealmSwift +import RxSwift +import SVProgressHUD + +class CheckController: UIViewController, MaskedTextFieldDelegateListener { + + @IBOutlet weak var number: UITextField! + @IBOutlet weak var check: UIButton! + @IBOutlet weak var history: UITableView! + + let bag = DisposeBag() + let maskFieldDelegate = MaskedTextFieldDelegate() + + override func viewDidLoad() { + super.viewDidLoad() + + self.maskFieldDelegate.primaryMaskFormat = "[A][000][AA] [009]" + self.maskFieldDelegate.listener = self + self.number.delegate = self.maskFieldDelegate + self.check.isEnabled = false + + let ds = RxTableViewRealmDataSource(cellIdentifier: "VehicleCell", cellType: VehicleCell.self) { cell, ip, vehicle in + cell.configure(with: vehicle) + } + ds.headerTitle = "History" + + let realm = try! Realm() + Observable.changeset(from: realm.objects(Vehicle.self)) + .bind(to: self.history.rx.realmChanges(ds)) + .disposed(by: self.bag) + + self.history.rx.realmModelSelected(Vehicle.self) + .subscribe(onNext: self.updateDetailController(with:)) + .disposed(by: self.bag) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: false) + } + + @IBAction func checkTapped(_ sender: UIButton) { + guard let number = self.number.text else { return } + + self.number.text = nil + SVProgressHUD.show() + Api.checkVehicle(by: number) + .observeOn(MainScheduler.instance) + .subscribe(onNext: onReceivedVehicle(_:), onError: { err in + SVProgressHUD.showError(withStatus: err.localizedDescription) + print(err.localizedDescription) + }).disposed(by: self.bag) + } + + func textField(_ textField: UITextField, didFillMandatoryCharacters complete: Bool, didExtractValue value: String) { + self.check.isEnabled = complete + print(value) + } + + func onReceivedVehicle(_ vehicle: Vehicle) { + if let realm = try? Realm() { + try? realm.write { + realm.add(vehicle) + } + } + + self.updateDetailController(with: vehicle) + SVProgressHUD.dismiss() + } + + 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) + } + } + } +} diff --git a/AutoCat/Controllers/MainSplitController.swift b/AutoCat/Controllers/MainSplitController.swift new file mode 100644 index 0000000..7bea3ab --- /dev/null +++ b/AutoCat/Controllers/MainSplitController.swift @@ -0,0 +1,58 @@ +import UIKit + +class MainSplitController: UISplitViewController, UISplitViewControllerDelegate { + + override func viewDidLoad() { + super.viewDidLoad() + self.preferredDisplayMode = .allVisible + self.delegate = self + } + + func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool { + if self.isCollapsed { + let tabController = self.viewControllers.first as? UITabBarController + let selectedNavController = tabController?.selectedViewController + + var vcToPush = vc + if let nav = vc as? UINavigationController { + vcToPush = nav.topViewController! + } + + vcToPush.hidesBottomBarWhenPushed = true + selectedNavController?.show(vcToPush, sender: self) + return true + } + + return false + } + + func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? + { + let tabController = splitViewController.viewControllers.first as? UITabBarController + let selectedNavController = tabController?.selectedViewController as? UINavigationController + + if selectedNavController?.topViewController is ReportController { + return selectedNavController?.popViewController(animated: false) + } + + return nil + } + + func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool + { + guard let detail = secondaryViewController as? ReportController else { return false } + + if detail.vehicle == nil { + return false + } + + if let tabController = primaryViewController as? UITabBarController { + let selectedNavController = tabController.selectedViewController as? UINavigationController + detail.hidesBottomBarWhenPushed = true + selectedNavController?.pushViewController(detail, animated: false) + return true + } + + return false + } +} diff --git a/AutoCat/Controllers/ReportController.swift b/AutoCat/Controllers/ReportController.swift new file mode 100644 index 0000000..6fc378b --- /dev/null +++ b/AutoCat/Controllers/ReportController.swift @@ -0,0 +1,217 @@ +import UIKit +import MagazineLayout + +enum ReportSection: Int, CaseIterable, CustomStringConvertible { + case header = 0 + case general = 1 + case identifiers = 2 + case photos = 3 + + var description: String { + switch self { + case .header: return "Header" + case .general: return "General" + case .identifiers: return "Identifiers" + case .photos: return "Photos" + } + } +} + +enum ReportGeneralSection: Int, CaseIterable, CustomStringConvertible { + case year = 0 + case category = 1 + case wheelPosition = 2 + case japanese = 3 + + var description: String { + switch self { + case .year: return "Year" + case .category: return "Category" + case .wheelPosition: return "Steering wheel position" + case .japanese: return "Japanese" + } + } +} + +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" + } + } +} + +class ReportController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateMagazineLayout { + + @IBOutlet weak var collection: UICollectionView! + + private let fullWidth = MagazineLayoutItemSizeMode(widthMode: .fullWidth(respectsHorizontalInsets: true), heightMode: .dynamic) + + var vehicle: Vehicle? { + didSet { + loadViewIfNeeded() + self.collection.reloadData() + } + } + + 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") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(self.traitCollection.horizontalSizeClass != .compact, animated: false) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + self.navigationController?.setNavigationBarHidden(self.traitCollection.horizontalSizeClass != .compact, animated: false) + } + + // MARK: - UICollectionViewDataSource + + func numberOfSections(in collectionView: UICollectionView) -> Int { + 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 .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 .category: + cell?.configure(param: generalSection.description, value: vehicle.category ?? "") + break + case .wheelPosition: + cell?.configure(param: generalSection.description, value: vehicle.isRightWheel ? "Right" : "Left") + break + case .japanese: + cell?.configure(param: generalSection.description, value: vehicle.isJapanese ? "Yes" : "No") + } + } + 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: + cell?.configure(param: idSection.description, value: vehicle.number) + break + case .vin: + cell?.configure(param: idSection.description, value: vehicle.vin1 ?? "") + break + case .sts: + cell?.configure(param: idSection.description, value: vehicle.sts ?? "") + break + case .pts: + cell?.configure(param: idSection.description, value: vehicle.pts ?? "") + 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.title.text = section.description + return sectionHeader + } + return UICollectionReusableView() + } + + // 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 .photos: return .init(widthMode: .thirdWidth, 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 .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 0 + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetsForSectionAtIndex index: Int) -> UIEdgeInsets + { + return .zero + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetsForItemsInSectionAtIndex index: Int) -> UIEdgeInsets + { + return .zero + } +} diff --git a/AutoCat/Info.plist b/AutoCat/Info.plist index 2a3483c..9592629 100644 --- a/AutoCat/Info.plist +++ b/AutoCat/Info.plist @@ -41,8 +41,6 @@ UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main UIRequiredDeviceCapabilities armv7 diff --git a/AutoCat/Models/Response.swift b/AutoCat/Models/Response.swift new file mode 100644 index 0000000..a66a87a --- /dev/null +++ b/AutoCat/Models/Response.swift @@ -0,0 +1,30 @@ +import Foundation + +class Response: Decodable where T: Decodable { + let success: Bool + let data: T? + let error: String? + + enum CodingKeys: String, CodingKey { + case success + case data + case error + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + success = try container.decode(Bool.self, forKey: .success) + if success { + do { + data = try container.decode(T.self, forKey: .data) + } catch { + print(error) + data = nil + } + error = nil + } else { + error = try container.decode(String.self, forKey: .error) + data = nil + } + } +} diff --git a/AutoCat/Models/Settings.swift b/AutoCat/Models/Settings.swift new file mode 100644 index 0000000..6d2d66c --- /dev/null +++ b/AutoCat/Models/Settings.swift @@ -0,0 +1,24 @@ +import Foundation + +class Settings { + private static let defaults = UserDefaults.standard + public static let shared = Settings() + + var user: User { + didSet { + if let json = try? JSONEncoder().encode(self.user) { + Settings.defaults.set(json, forKey: "user") + Settings.defaults.synchronize() + } + } + } + + init() { + self.user = Settings.getUser() + } + + private static func getUser() -> User { + guard let data = Settings.defaults.data(forKey: "user") else { return User() } + return (try? JSONDecoder().decode(User.self, from: data)) ?? User() + } +} diff --git a/AutoCat/Models/User.swift b/AutoCat/Models/User.swift new file mode 100644 index 0000000..43b15d3 --- /dev/null +++ b/AutoCat/Models/User.swift @@ -0,0 +1,11 @@ +import Foundation + +class User: Codable { + let login: String + let token: String + + init() { + self.login = "" + self.token = "" + } +} diff --git a/AutoCat/Models/Vehicle.swift b/AutoCat/Models/Vehicle.swift new file mode 100644 index 0000000..0d4642e --- /dev/null +++ b/AutoCat/Models/Vehicle.swift @@ -0,0 +1,51 @@ +import Foundation +import RealmSwift + +class VehicleName: Object, Decodable { + @objc dynamic var original: String? + @objc dynamic var normalized: String? +} + +class VehicleBrand: Object, Decodable { + @objc dynamic var name: VehicleName? + @objc dynamic var logo: String? +} + +class VehicleModel: Object, Decodable { + @objc dynamic var name: VehicleName? +} + +class VehicleEngine: Object, Decodable { + @objc dynamic var number: String? + @objc dynamic var volume: Int = 0 + @objc dynamic var powerHp: Float = 0 + @objc dynamic var powerKw: Float = 0 + @objc dynamic var fuelType: String? +} + +class VehiclePhoto: Object, Decodable { + @objc dynamic var brand: String? + @objc dynamic var model: String? + @objc dynamic var date: Int64 = 0 + @objc dynamic var url: String = "" +} + +class Vehicle: Object, Decodable { + @objc dynamic var _id: String = "" + @objc dynamic var brand: VehicleBrand? + @objc dynamic var model: VehicleModel? + @objc dynamic var color: String? + @objc dynamic var year: Int = 0 + @objc dynamic var category: String? + @objc dynamic var engine: VehicleEngine? + @objc dynamic var number: String = "" + @objc dynamic var vin1: String? + @objc dynamic var vin2: String? + @objc dynamic var sts: String? + @objc dynamic var pts: String? + @objc dynamic var isRightWheel: Bool = false + @objc dynamic var isJapanese: Bool = false + @objc dynamic var addedDate: Int64 = 0 + @objc dynamic var addedBy: String = "" + let photos = List() +} diff --git a/AutoCat/SceneDelegate.swift b/AutoCat/SceneDelegate.swift index c960bee..696caeb 100644 --- a/AutoCat/SceneDelegate.swift +++ b/AutoCat/SceneDelegate.swift @@ -1,23 +1,25 @@ -// -// SceneDelegate.swift -// AutoCat -// -// Created by Selim Mustafaev on 20.02.2020. -// Copyright © 2020 Selim Mustafaev. All rights reserved. -// - import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + guard let windowScene = (scene as? UIWindowScene) else { return } + + self.window = UIWindow(windowScene: windowScene) + let storyboard = UIStoryboard(name: "Main", bundle: nil) + + if Settings.shared.user.token.isEmpty { + self.window?.rootViewController = storyboard.instantiateViewController(identifier: "AuthController") + } else { + self.window?.rootViewController = storyboard.instantiateViewController(identifier: "MainSplitController") + } + + self.window?.makeKeyAndVisible() } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/AutoCat/ThirdParty/RxRealmDataSources/Reactive+RxRealmDataSources.swift b/AutoCat/ThirdParty/RxRealmDataSources/Reactive+RxRealmDataSources.swift new file mode 100644 index 0000000..c51ca48 --- /dev/null +++ b/AutoCat/ThirdParty/RxRealmDataSources/Reactive+RxRealmDataSources.swift @@ -0,0 +1,113 @@ +// +// RxRealm extensions +// +// Copyright (c) 2016 RxSwiftCommunity. All rights reserved. +// Check the LICENSE file for details +// + +import Foundation + +import RealmSwift +import RxSwift +import RxCocoa +import RxRealm + +#if os(iOS) +// MARK: - iOS / UIKit + +import UIKit +extension Reactive where Base: UITableView { + + public func realmChanges(_ dataSource: RxTableViewRealmDataSource) + -> RealmBindObserver, RxTableViewRealmDataSource> { + + return RealmBindObserver(dataSource: dataSource) {ds, results, changes in + if ds.tableView == nil { + ds.tableView = self.base + } + ds.tableView?.dataSource = ds + ds.applyChanges(items: AnyRealmCollection(results), changes: changes) + } + } + + public func realmModelSelected(_ modelType: E.Type) -> ControlEvent where E: RealmSwift.Object { + + let source: Observable = self.itemSelected.flatMap { [weak view = self.base as UITableView] indexPath -> Observable in + guard let view = view, let ds = view.dataSource as? RxTableViewRealmDataSource else { + return Observable.empty() + } + + return Observable.just(ds.model(at: indexPath)) + } + + return ControlEvent(events: source) + } + +} + +extension Reactive where Base: UICollectionView { + + public func realmChanges(_ dataSource: RxCollectionViewRealmDataSource) + -> RealmBindObserver, RxCollectionViewRealmDataSource> { + + return RealmBindObserver(dataSource: dataSource) {ds, results, changes in + if ds.collectionView == nil { + ds.collectionView = self.base + } + ds.collectionView?.dataSource = ds + ds.applyChanges(items: AnyRealmCollection(results), changes: changes) + } + } + + public func realmModelSelected(_ modelType: E.Type) -> ControlEvent where E: RealmSwift.Object { + + let source: Observable = self.itemSelected.flatMap { [weak view = self.base as UICollectionView] indexPath -> Observable in + guard let view = view, let ds = view.dataSource as? RxCollectionViewRealmDataSource else { + return Observable.empty() + } + + return Observable.just(ds.model(at: indexPath)) + } + + return ControlEvent(events: source) + } +} + + +#elseif os(OSX) +// MARK: - macOS / Cocoa + +import Cocoa +extension Reactive where Base: NSTableView { + + public func realmChanges(_ dataSource: RxTableViewRealmDataSource) + -> RealmBindObserver, RxTableViewRealmDataSource> { + + base.delegate = dataSource + base.dataSource = dataSource + + return RealmBindObserver(dataSource: dataSource) {ds, results, changes in + if dataSource.tableView == nil { + dataSource.tableView = self.base + } + ds.applyChanges(items: AnyRealmCollection(results), changes: changes) + } + } +} + +extension Reactive where Base: NSCollectionView { + + public func realmChanges(_ dataSource: RxCollectionViewRealmDataSource) + -> RealmBindObserver, RxCollectionViewRealmDataSource> { + + return RealmBindObserver(dataSource: dataSource) {ds, results, changes in + if ds.collectionView == nil { + ds.collectionView = self.base + } + ds.collectionView?.dataSource = ds + ds.applyChanges(items: AnyRealmCollection(results), changes: changes) + } + } +} + +#endif diff --git a/AutoCat/ThirdParty/RxRealmDataSources/RealmBindObserver.swift b/AutoCat/ThirdParty/RxRealmDataSources/RealmBindObserver.swift new file mode 100644 index 0000000..a60b378 --- /dev/null +++ b/AutoCat/ThirdParty/RxRealmDataSources/RealmBindObserver.swift @@ -0,0 +1,41 @@ +// +// RxRealm extensions +// +// Copyright (c) 2016 RxSwiftCommunity. All rights reserved. +// Check the LICENSE file for details +// + +import Foundation + +import RealmSwift +import RxSwift +import RxCocoa +import RxRealm + +public class RealmBindObserver: ObserverType { + typealias BindingType = (DS, C, RealmChangeset?) -> Void + public typealias E = (C, RealmChangeset?) + + let dataSource: DS + let binding: BindingType + + init(dataSource: DS, binding: @escaping BindingType) { + self.dataSource = dataSource + self.binding = binding + } + + public func on(_ event: Event) { + switch event { + case .next(let element): + binding(dataSource, element.0, element.1) + case .error: + return + case .completed: + return + } + } + + func asObserver() -> AnyObserver { + return AnyObserver(eventHandler: on) + } +} diff --git a/AutoCat/ThirdParty/RxRealmDataSources/RxCollectionViewRealmDataSource.swift b/AutoCat/ThirdParty/RxRealmDataSources/RxCollectionViewRealmDataSource.swift new file mode 100644 index 0000000..8883732 --- /dev/null +++ b/AutoCat/ThirdParty/RxRealmDataSources/RxCollectionViewRealmDataSource.swift @@ -0,0 +1,210 @@ +// +// RxRealm extensions +// +// Copyright (c) 2016 RxSwiftCommunity. All rights reserved. +// Check the LICENSE file for details +// + +import Foundation + +import RealmSwift +import RxSwift +import RxCocoa +import RxRealm + +#if os(iOS) + // MARK: - iOS / UIKit + + import UIKit + + public typealias CollectionCellFactory = (RxCollectionViewRealmDataSource, UICollectionView, IndexPath, E) -> UICollectionViewCell + public typealias CollectionCellConfig = (CellType, IndexPath, E) -> Void + + open class RxCollectionViewRealmDataSource : NSObject, UICollectionViewDataSource { + private var items: AnyRealmCollection? + + // MARK: - Configuration + + public var collectionView: UICollectionView? + public var animated = true + + // MARK: - Init + public let cellIdentifier: String + public let cellFactory: CollectionCellFactory + + public init(cellIdentifier: String, cellFactory: @escaping CollectionCellFactory) { + self.cellIdentifier = cellIdentifier + self.cellFactory = cellFactory + } + + public init(cellIdentifier: String, cellType: CellType.Type, cellConfig: @escaping CollectionCellConfig) where CellType: UICollectionViewCell { + self.cellIdentifier = cellIdentifier + self.cellFactory = {ds, cv, ip, model in + let cell = cv.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: ip) as! CellType + cellConfig(cell, ip, model) + return cell + } + } + + // MARK: - Data access + public func model(at indexPath: IndexPath) -> E { + return items![indexPath.row] + } + + // MARK: - UICollectionViewDataSource protocol + public func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return items?.count ?? 0 + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + return cellFactory(self, collectionView, indexPath, items![indexPath.row]) + } + + // MARK: - Applying changeset to the collection view + private let fromRow = {(row: Int) in return IndexPath(row: row, section: 0)} + + func applyChanges(items: AnyRealmCollection, changes: RealmChangeset?) { + if self.items == nil { + self.items = items + } + + guard let collectionView = collectionView else { + fatalError("You have to bind a collection view to the data source.") + } + + guard animated else { + collectionView.reloadData() + return + } + + guard let changes = changes else { + collectionView.reloadData() + return + } + + let lastItemCount = collectionView.numberOfItems(inSection: 0) + guard items.count == lastItemCount + changes.inserted.count - changes.deleted.count else { + collectionView.reloadData() + return + } + + collectionView.performBatchUpdates({[unowned self] in + collectionView.deleteItems(at: changes.deleted.map(self.fromRow)) + collectionView.reloadItems(at: changes.updated.map(self.fromRow)) + collectionView.insertItems(at: changes.inserted.map(self.fromRow)) + }, completion: nil) + } +} + +#elseif os(OSX) +// MARK: - macOS / Cocoa + +import Cocoa + + public typealias CollectionItemFactory = (RxCollectionViewRealmDataSource, NSCollectionView, IndexPath, E) -> NSCollectionViewItem + public typealias CollectionItemConfig = (ItemType, IndexPath, E) -> Void + + open class RxCollectionViewRealmDataSource : NSObject, NSCollectionViewDataSource { + + private var items: AnyRealmCollection? + + // MARK: - Configuration + + public var collectionView: NSCollectionView? + public var animated = true + + // MARK: - Init + public let itemIdentifier: String + public let itemFactory: CollectionItemFactory + + public weak var delegate: NSCollectionViewDelegate? + public weak var dataSource: NSCollectionViewDataSource? + + public init(itemIdentifier: String, itemFactory: @escaping CollectionItemFactory) { + self.itemIdentifier = itemIdentifier + self.itemFactory = itemFactory + } + + public init(itemIdentifier: String, itemType: ItemType.Type, itemConfig: @escaping CollectionItemConfig) where ItemType: NSCollectionViewItem { + self.itemIdentifier = itemIdentifier + self.itemFactory = { ds, cv, ip, model in + let item = cv.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: itemIdentifier), for: ip) as! ItemType + itemConfig(item, ip, model) + return item + } + } + + // MARK: - NSCollectionViewDataSource protocol + public func numberOfSections(in collectionView: NSCollectionView) -> Int { + return 1 + } + + public func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + return items?.count ?? 0 + } + + @available(OSX 10.11, *) + public func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + return itemFactory(self, collectionView, indexPath, items![indexPath.item]) + } + + // MARK: - Proxy unimplemented data source and delegate methods + open override func responds(to aSelector: Selector!) -> Bool { + if RxCollectionViewRealmDataSource.instancesRespond(to: aSelector) { + return true + } else if let delegate = delegate { + return delegate.responds(to: aSelector) + } else if let dataSource = dataSource { + return dataSource.responds(to: aSelector) + } else { + return false + } + } + + open override func forwardingTarget(for aSelector: Selector!) -> Any? { + return delegate ?? dataSource + } + + // MARK: - Applying changeset to the collection view + private let fromRow = {(row: Int) in return IndexPath(item: row, section: 0)} + + func applyChanges(items: AnyRealmCollection, changes: RealmChangeset?) { + if self.items == nil { + self.items = items + } + + guard let collectionView = collectionView else { + fatalError("You have to bind a collection view to the data source.") + } + + guard animated else { + collectionView.reloadData() + return + } + + guard let changes = changes else { + collectionView.reloadData() + return + } + + let lastItemCount = collectionView.numberOfItems(inSection: 0) + guard items.count == lastItemCount + changes.inserted.count - changes.deleted.count else { + collectionView.reloadData() + return + } + + collectionView.performBatchUpdates({[unowned self] in + //TODO: this should be animated, but doesn't seem to be? + collectionView.animator().deleteItems(at: Set(changes.deleted.map(self.fromRow))) + collectionView.animator().reloadItems(at: Set(changes.updated.map(self.fromRow))) + collectionView.animator().insertItems(at: Set(changes.inserted.map(self.fromRow))) + }, completionHandler: nil) + } + } + + +#endif diff --git a/AutoCat/ThirdParty/RxRealmDataSources/RxTableViewRealmDataSource.swift b/AutoCat/ThirdParty/RxRealmDataSources/RxTableViewRealmDataSource.swift new file mode 100644 index 0000000..c80bb80 --- /dev/null +++ b/AutoCat/ThirdParty/RxRealmDataSources/RxTableViewRealmDataSource.swift @@ -0,0 +1,223 @@ +// +// RxRealm extensions +// +// Copyright (c) 2016 RxSwiftCommunity. All rights reserved. +// Check the LICENSE file for details +// + +import Foundation + +import RealmSwift +import RxSwift +import RxCocoa +import RxRealm + +#if os(iOS) + // MARK: - iOS / UIKit + + import UIKit + + public typealias TableCellFactory = (RxTableViewRealmDataSource, UITableView, IndexPath, E) -> UITableViewCell + public typealias TableCellConfig = (CellType, IndexPath, E) -> Void + + open class RxTableViewRealmDataSource: NSObject, UITableViewDataSource { + + private var items: AnyRealmCollection? + + // MARK: - Configuration + + public var tableView: UITableView? + public var animated = true + public var rowAnimations = ( + insert: UITableView.RowAnimation.automatic, + update: UITableView.RowAnimation.automatic, + delete: UITableView.RowAnimation.automatic) + + public var headerTitle: String? + public var footerTitle: String? + + // MARK: - Init + public let cellIdentifier: String + public let cellFactory: TableCellFactory + + public init(cellIdentifier: String, cellFactory: @escaping TableCellFactory) { + self.cellIdentifier = cellIdentifier + self.cellFactory = cellFactory + } + + public init(cellIdentifier: String, cellType: CellType.Type, cellConfig: @escaping TableCellConfig) where CellType: UITableViewCell { + self.cellIdentifier = cellIdentifier + self.cellFactory = {ds, tv, ip, model in + let cell = tv.dequeueReusableCell(withIdentifier: cellIdentifier, for: ip) as! CellType + cellConfig(cell, ip, model) + return cell + } + } + + // MARK: - Data access + public func model(at indexPath: IndexPath) -> E { + return items![indexPath.row] + } + + // MARK: - UITableViewDataSource protocol + public func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return items?.count ?? 0 + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return cellFactory(self, tableView, indexPath, items![indexPath.row]) + } + + public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return headerTitle + } + + public func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + return footerTitle + } + + // MARK: - Applying changeset to the table view + private let fromRow = {(row: Int) in return IndexPath(row: row, section: 0)} + + func applyChanges(items: AnyRealmCollection, changes: RealmChangeset?) { + if self.items == nil { + self.items = items + } + + guard let tableView = tableView else { + fatalError("You have to bind a table view to the data source.") + } + + guard animated else { + tableView.reloadData() + return + } + + guard let changes = changes else { + tableView.reloadData() + return + } + + let lastItemCount = tableView.numberOfRows(inSection: 0) + guard items.count == lastItemCount + changes.inserted.count - changes.deleted.count else { + tableView.reloadData() + return + } + + tableView.beginUpdates() + tableView.deleteRows(at: changes.deleted.map(fromRow), with: rowAnimations.delete) + tableView.insertRows(at: changes.inserted.map(fromRow), with: rowAnimations.insert) + tableView.reloadRows(at: changes.updated.map(fromRow), with: rowAnimations.update) + tableView.endUpdates() + } + } + +#elseif os(OSX) + // MARK: - macOS / Cocoa + + import Cocoa + + public typealias TableCellFactory = (RxTableViewRealmDataSource, NSTableView, Int, E) -> NSTableCellView + public typealias TableCellConfig = (CellType, Int, E) -> Void + + open class RxTableViewRealmDataSource: NSObject, NSTableViewDataSource, NSTableViewDelegate { + + private var items: AnyRealmCollection? + + // MARK: - Configuration + + public var tableView: NSTableView? + public var animated = true + public var rowAnimations = ( + insert: NSTableView.AnimationOptions.effectFade, + update: NSTableView.AnimationOptions.effectFade, + delete: NSTableView.AnimationOptions.effectFade) + + public weak var delegate: NSTableViewDelegate? + public weak var dataSource: NSTableViewDataSource? + + // MARK: - Init + public let cellIdentifier: String + public let cellFactory: TableCellFactory + + public init(cellIdentifier: String, cellFactory: @escaping TableCellFactory) { + self.cellIdentifier = cellIdentifier + self.cellFactory = cellFactory + } + + public init(cellIdentifier: String, cellType: CellType.Type, cellConfig: @escaping TableCellConfig) where CellType: NSTableCellView { + self.cellIdentifier = cellIdentifier + self.cellFactory = { ds, tv, row, model in + let cell = tv.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: cellIdentifier), owner: tv) as! CellType + cellConfig(cell, row, model) + return cell + } + } + + // MARK: - UITableViewDataSource protocol + public func numberOfRows(in tableView: NSTableView) -> Int { + return items?.count ?? 0 + } + + public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + return cellFactory(self, tableView, row, items![row]) + } + + // MARK: - Proxy unimplemented data source and delegate methods + open override func responds(to aSelector: Selector!) -> Bool { + if RxTableViewRealmDataSource.instancesRespond(to: aSelector) { + return true + } else if let delegate = delegate { + return delegate.responds(to: aSelector) + } else if let dataSource = dataSource { + return dataSource.responds(to: aSelector) + } else { + return false + } + } + + open override func forwardingTarget(for aSelector: Selector!) -> Any? { + return delegate ?? dataSource + } + + // MARK: - Applying changeset to the table view + private let fromRow = {(row: Int) in return IndexPath(item: row, section: 0)} + + func applyChanges(items: AnyRealmCollection, changes: RealmChangeset?) { + if self.items == nil { + self.items = items + } + + guard let tableView = tableView else { + fatalError("You have to bind a table view to the data source.") + } + + guard animated else { + tableView.reloadData() + return + } + + guard let changes = changes else { + tableView.reloadData() + return + } + + let lastItemCount = tableView.numberOfRows + guard items.count == lastItemCount + changes.inserted.count - changes.deleted.count else { + tableView.reloadData() + return + } + + tableView.beginUpdates() + tableView.removeRows(at: IndexSet(changes.deleted), withAnimation: rowAnimations.delete) + tableView.insertRows(at: IndexSet(changes.inserted), withAnimation: rowAnimations.insert) + tableView.reloadData(forRowIndexes: IndexSet(changes.updated), columnIndexes: IndexSet([0])) + tableView.endUpdates() + } + } + +#endif diff --git a/AutoCat/Utils/Api.swift b/AutoCat/Utils/Api.swift new file mode 100644 index 0000000..4aa6d42 --- /dev/null +++ b/AutoCat/Utils/Api.swift @@ -0,0 +1,69 @@ +import Foundation +import RxSwift + +class Api { + private static let baseUrl = "https://vps.aliencat.pro:8443/" + + private static func genError(_ msg: String, suggestion: String, code: Int = 0) -> Error { + 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 } + + var request = URLRequest(url: 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 { + 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 { + guard let request = self.createRequest(api: api, method: method, body: body) else { + return Observable.error(self.genError("Error creating request", suggestion: "")) + } + + return URLSession.shared.rx.data(request: request).map { data in +// let str = String(data: data, encoding: .utf8) +// print("================================") +// print(str) +// print("================================") + let resp = try JSONDecoder().decode(Response.self, from: data) + if resp.success { + return resp.data! + } else { + throw self.genError(resp.error!, suggestion: "") + } + } + } + + public static func login(username: String, password: String) -> Observable { + let body = [ + "login": username, + "password": password + ] + return self.makeRequest(api: "user/login", method: "POST", body: body) + } + + public static func signup(username: String, password: String) -> Observable { + let body = [ + "login": username, + "password": password + ] + 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 checkVehicle(by number: String) -> Observable { + return self.makeRequest(api: "vehicles/check", method: "POST", body: ["number": number]) + } +} diff --git a/AutoCat/ViewController.swift b/AutoCat/ViewController.swift deleted file mode 100644 index 943b892..0000000 --- a/AutoCat/ViewController.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ViewController.swift -// AutoCat -// -// Created by Selim Mustafaev on 20.02.2020. -// Copyright © 2020 Selim Mustafaev. All rights reserved. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - -} - diff --git a/Cartfile b/Cartfile new file mode 100644 index 0000000..a8367fd --- /dev/null +++ b/Cartfile @@ -0,0 +1 @@ +github "selim-mustafaev/SVProgressHUD" "ios13-support" diff --git a/Cartfile.resolved b/Cartfile.resolved new file mode 100644 index 0000000..60563a2 --- /dev/null +++ b/Cartfile.resolved @@ -0,0 +1 @@ +github "selim-mustafaev/SVProgressHUD" "17b1eacb2800ffa91eb5fb12e1a5983dba29f9dc" diff --git a/input.xcfilelist b/input.xcfilelist new file mode 100644 index 0000000..368972b --- /dev/null +++ b/input.xcfilelist @@ -0,0 +1 @@ +$(SRCROOT)/Carthage/Build/iOS/SVProgressHUD.framework diff --git a/output.xcfilelist b/output.xcfilelist new file mode 100644 index 0000000..2f4482c --- /dev/null +++ b/output.xcfilelist @@ -0,0 +1 @@ +$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/SVProgressHUD.framework