Store GPS coordinates when check plate number or recording audio

This commit is contained in:
Selim Mustafaev 2020-08-21 15:43:57 +03:00
parent e8212e0184
commit c39971637d
24 changed files with 655 additions and 211 deletions

View File

@ -33,6 +33,7 @@
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 */; };
7A15051224DB3E3000F39631 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A15051124DB3E3000F39631 /* AnyEncodable.swift */; };
7A27ADC7249D43210035F39E /* RegionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADC6249D43210035F39E /* RegionsController.swift */; };
7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF2249F8B650035F39E /* RecordsController.swift */; };
7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */; };
@ -84,6 +85,7 @@
7AB562BA249C9E9B00473D53 /* Region.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB562B9249C9E9B00473D53 /* Region.swift */; };
7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8B2435C38700258F61 /* CustomTextField.swift */; };
7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8D2435D1A000258F61 /* CustomButton.swift */; };
7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */; };
7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFE727240455E200910EB7 /* SettingsController.swift */; };
7AF58D2F24029C5200CE01A0 /* MagazineLayout in Frameworks */ = {isa = PBXBuildFile; productRef = 7AF58D2E24029C5200CE01A0 /* MagazineLayout */; };
7AF58D3124029E1000CE01A0 /* VehicleHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF58D3024029E1000CE01A0 /* VehicleHeaderCell.swift */; };
@ -112,6 +114,7 @@
7A11474823FF2B2D00B424AF /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; };
7A11474A23FF368B00B424AF /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
7A11474D23FFEE8800B424AF /* SVProgressHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SVProgressHUD.framework; path = Carthage/Build/iOS/SVProgressHUD.framework; sourceTree = "<group>"; };
7A15051124DB3E3000F39631 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
7A27ADC6249D43210035F39E /* RegionsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionsController.swift; sourceTree = "<group>"; };
7A27ADF2249F8B650035F39E /* RecordsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsController.swift; sourceTree = "<group>"; };
7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExt.swift; sourceTree = "<group>"; };
@ -162,6 +165,7 @@
7AB562B9249C9E9B00473D53 /* Region.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Region.swift; sourceTree = "<group>"; };
7AB67E8B2435C38700258F61 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; };
7AB67E8D2435D1A000258F61 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = "<group>"; };
7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExt.swift; sourceTree = "<group>"; };
7AEFE727240455E200910EB7 /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = "<group>"; };
7AF58D3024029E1000CE01A0 /* VehicleHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleHeaderCell.swift; sourceTree = "<group>"; };
7AF58D57240309CA00CE01A0 /* VehicleTextParamCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleTextParamCell.swift; sourceTree = "<group>"; };
@ -258,6 +262,7 @@
7A64AE6E2469DFB600ABE48E /* ATGMediaBrowser */,
7A6DD90724329144009DE740 /* CenterTextLayer.swift */,
7A96AE32246C095700297C33 /* Base64FS.swift */,
7A15051124DB3E3000F39631 /* AnyEncodable.swift */,
);
path = ThirdParty;
sourceTree = "<group>";
@ -311,6 +316,7 @@
7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */,
7A27ADF824A09CAD0035F39E /* CocoaError.swift */,
7A659B5A24A3768A0043A0F2 /* Substrings.swift */,
7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -500,6 +506,7 @@
7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */,
7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */,
7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */,
7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */,
7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */,
7A8A2209248D10EC0073DFD9 /* ResizeImage.swift in Sources */,
7A6DD90E24337930009DE740 /* PlateNumber.swift in Sources */,
@ -522,6 +529,7 @@
7A7547DD2403180A004E8406 /* SectionHeader.swift in Sources */,
7AF58D58240309CA00CE01A0 /* VehicleTextParamCell.swift in Sources */,
7A96AE2D246B2B7400297C33 /* GoogleSignInController.swift in Sources */,
7A15051224DB3E3000F39631 /* AnyEncodable.swift in Sources */,
7A1090EA24A3A26300B4F0B2 /* AudioPlayer.swift in Sources */,
7A11474723FF2AA500B424AF /* User.swift in Sources */,
7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */,
@ -691,7 +699,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 21;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = 46DTTB8X4S;
INFOPLIST_FILE = AutoCat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
@ -713,7 +721,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 21;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = 46DTTB8X4S;
INFOPLIST_FILE = AutoCat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;

View File

@ -24,8 +24,8 @@
"repositoryURL": "https://github.com/onevcat/Kingfisher",
"state": {
"branch": null,
"revision": "46bf251fee8ed426921c647790f08ca8ad0105a9",
"version": "5.13.1"
"revision": "1339ebea9498ef6c3fc75cc195d7163d7c7167f9",
"version": "5.14.1"
}
},
{
@ -33,8 +33,8 @@
"repositoryURL": "https://github.com/airbnb/MagazineLayout",
"state": {
"branch": null,
"revision": "bbbe1456c34c1abb527d05ff9da3ff2a54584d78",
"version": "1.5.5"
"revision": "12dd2cc84b7f17c4f46c7d95cde64d521c588ee8",
"version": "1.6.1"
}
},
{
@ -78,8 +78,8 @@
"repositoryURL": "https://github.com/ReactiveX/RxSwift.git",
"state": {
"branch": null,
"revision": "b3e888b4972d9bc76495dd74d30a8c7fad4b9395",
"version": "5.0.1"
"revision": "002d325b0bdee94e7882e1114af5ff4fe1e96afa",
"version": "5.1.1"
}
},
{

View File

@ -50,64 +50,16 @@
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "0F1972A7-94E8-40E5-834C-B873D2578DC7"
shouldBeEnabled = "Yes"
uuid = "339C709C-134A-4B97-9F9C-949886D4AD65"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "AutoCat/Controllers/RecordsController.swift"
filePath = "AutoCat/Controllers/SearchController.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "150"
endingLineNumber = "150"
landmarkName = "onAddVoiceRecord(_:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "79DDC3DB-613E-40E9-AD33-AEBAA5775C62"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "AutoCat/Controllers/RecordsController.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "144"
endingLineNumber = "144"
landmarkName = "onAddVoiceRecord(_:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "664BF878-7362-4887-BFD4-6938FCAB4750"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "AutoCat/Utils/Location.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "46"
endingLineNumber = "46"
landmarkName = "locationManager(_:didFailWithError:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "3661DA4A-8777-45F2-A6FD-0F1D3F487FF8"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "AutoCat/Utils/Location.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "40"
endingLineNumber = "40"
landmarkName = "locationManager(_:didUpdateLocations:)"
startingLineNumber = "94"
endingLineNumber = "94"
landmarkName = "onFilter(_:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>

View File

@ -11,7 +11,7 @@ extension OSLog {
enum QuickAction {
case none
case check
case checkNumber(String)
case checkNumber(String, VehicleEvent?)
case addVoiceRecord
}
@ -23,7 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let config = Realm.Configuration(
schemaVersion: 10,
schemaVersion: 13,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion <= 3 {
var numbers: [String] = []

View File

@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
</dict>
</plist>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pme-aR-UNJ">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pme-aR-UNJ">
<device id="retina4_7" orientation="portrait" appearance="dark"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
@ -40,9 +40,9 @@
<constraint firstAttribute="height" constant="48" id="Tk0-8G-9hP"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ysk-FW-p2W">
<rect key="frame" x="80" y="26" width="215" height="33.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ysk-FW-p2W">
<rect key="frame" x="80" y="29.5" width="215" height="26.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
@ -64,10 +64,10 @@
</connections>
</collectionViewCell>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="VehicleTextParamCell" id="3Qa-Fn-qe7" customClass="VehicleTextParamCell" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="0.0" y="118" width="145" height="42.5"/>
<rect key="frame" x="0.0" y="118" width="145.5" height="42.5"/>
<autoresizingMask key="autoresizingMask"/>
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="fmb-JM-Fcr">
<rect key="frame" x="0.0" y="0.0" width="145" height="42.5"/>
<rect key="frame" x="0.0" y="0.0" width="145.5" height="42.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="023-Q6-kPk">
@ -76,14 +76,23 @@
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ptE-4B-jxt">
<rect key="frame" x="76.5" y="8" width="52.5" height="26.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="Value" textAlignment="right" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="xoM-AA-KQ5" customClass="CustomTextField" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="76.5" y="8" width="53" height="27"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<textInputTraits key="textInputTraits"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="editable" value="NO"/>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="0.0"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<real key="value" value="0.0"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</textField>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WAm-et-t5P">
<rect key="frame" x="0.0" y="41.5" width="145" height="1"/>
<rect key="frame" x="0.0" y="41.5" width="145.5" height="1"/>
<color key="backgroundColor" systemColor="separatorColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="Mpt-xA-af2"/>
@ -91,14 +100,14 @@
</view>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="ptE-4B-jxt" secondAttribute="trailing" constant="16" id="4EF-1X-hrJ"/>
<constraint firstAttribute="bottom" secondItem="WAm-et-t5P" secondAttribute="bottom" id="91A-1k-B4C"/>
<constraint firstItem="ptE-4B-jxt" firstAttribute="leading" secondItem="023-Q6-kPk" secondAttribute="trailing" constant="8" id="BPP-fC-Q8I"/>
<constraint firstItem="023-Q6-kPk" firstAttribute="centerY" secondItem="ptE-4B-jxt" secondAttribute="centerY" id="Ce2-tJ-wbq"/>
<constraint firstItem="WAm-et-t5P" firstAttribute="leading" secondItem="fmb-JM-Fcr" secondAttribute="leading" id="Qr4-Eb-9YL"/>
<constraint firstItem="xoM-AA-KQ5" firstAttribute="centerY" secondItem="fmb-JM-Fcr" secondAttribute="centerY" id="TRj-nX-R3d"/>
<constraint firstItem="xoM-AA-KQ5" firstAttribute="leading" secondItem="023-Q6-kPk" secondAttribute="trailing" constant="8" id="Vip-dt-0kM"/>
<constraint firstAttribute="bottom" secondItem="023-Q6-kPk" secondAttribute="bottom" constant="8" id="asT-vZ-W1W"/>
<constraint firstItem="023-Q6-kPk" firstAttribute="top" secondItem="fmb-JM-Fcr" secondAttribute="top" constant="8" id="i2x-Tv-BGw"/>
<constraint firstItem="023-Q6-kPk" firstAttribute="leading" secondItem="fmb-JM-Fcr" secondAttribute="leading" constant="16" id="j9m-4c-c7c"/>
<constraint firstAttribute="trailing" secondItem="xoM-AA-KQ5" secondAttribute="trailing" constant="16" id="oAu-y7-Ano"/>
<constraint firstAttribute="trailing" secondItem="WAm-et-t5P" secondAttribute="trailing" id="zil-58-Wao"/>
</constraints>
</collectionViewCellContentView>
@ -106,11 +115,11 @@
<connections>
<outlet property="dividerHeightConstraint" destination="Mpt-xA-af2" id="bYb-0D-Lz0"/>
<outlet property="paramName" destination="023-Q6-kPk" id="pGj-6y-fbP"/>
<outlet property="paramValue" destination="ptE-4B-jxt" id="4YD-Va-YLb"/>
<outlet property="paramValue" destination="xoM-AA-KQ5" id="2Q2-ig-dIW"/>
</connections>
</collectionViewCell>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="VehiclePhotoCell" id="X77-i3-DVY" customClass="VehiclePhotoCell" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="155" y="95" width="58" height="88.5"/>
<rect key="frame" x="155.5" y="95" width="58" height="88.5"/>
<autoresizingMask key="autoresizingMask"/>
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="AMu-PZ-gGR">
<rect key="frame" x="0.0" y="0.0" width="58" height="88.5"/>
@ -223,14 +232,14 @@
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="VehicleCell" id="VEP-QD-i6y" customClass="VehicleCell" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="85"/>
<rect key="frame" x="0.0" y="28" width="375" height="85.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VEP-QD-i6y" id="8hH-8I-XLB">
<rect key="frame" x="0.0" y="0.0" width="375" height="85"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="85.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Kia (JF) Optima" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AQY-7N-q8D">
<rect key="frame" x="8" y="8" width="124" height="21"/>
<rect key="frame" x="8" y="8" width="124" height="21.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -242,7 +251,7 @@
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cvf-vM-QnT" customClass="PlateView" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="8" y="37" width="317" height="40"/>
<rect key="frame" x="8" y="37.5" width="317" height="40"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="Xoz-Iw-PCU"/>
@ -495,6 +504,14 @@
<rect key="frame" x="0.0" y="0.0" width="250" height="50.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle0"/>
<textInputTraits key="textInputTraits" returnKeyType="done"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<real key="value" value="6"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</textField>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ync-fd-xQI" customClass="CustomButton" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="0.0" y="66.5" width="250" height="48"/>
@ -515,7 +532,7 @@
<constraint firstAttribute="width" constant="250" id="lqO-Yy-NyQ"/>
</constraints>
</stackView>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="JKr-UE-x8f">
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="onDrag" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="JKr-UE-x8f">
<rect key="frame" x="0.0" y="206.5" width="375" height="411.5"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
@ -677,6 +694,14 @@
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<real key="value" value="6"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</textField>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Password" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="G1p-Hz-8yn" customClass="CustomTextField" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="0.0" y="50" width="262.5" height="34"/>
@ -685,6 +710,14 @@
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" secureTextEntry="YES"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<real key="value" value="6"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</textField>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ltG-B1-UBj" customClass="CustomButton" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="0.0" y="100" width="262.5" height="40"/>
@ -830,11 +863,11 @@
<image name="doc.on.doc" catalog="system" width="117" height="128"/>
<image name="line.horizontal.3.decrease" catalog="system" width="128" height="73"/>
<image name="play.fill" catalog="system" width="116" height="128"/>
<image name="record" width="128" height="128"/>
<image name="record" width="31" height="31"/>
<image name="record-compact" width="23" height="23"/>
<image name="search" width="128" height="128"/>
<image name="search" width="23" height="23"/>
<image name="search-compact" width="17" height="17"/>
<image name="settings" width="128" height="128"/>
<image name="settings" width="25" height="25"/>
<image name="settings-compact" width="18" height="18"/>
</resources>
</document>

View File

@ -3,7 +3,7 @@ import MagazineLayout
class VehicleTextParamCell: MagazineLayoutCollectionViewCell {
@IBOutlet weak var paramName: UILabel!
@IBOutlet weak var paramValue: UILabel!
@IBOutlet weak var paramValue: UITextField!
@IBOutlet weak var dividerHeightConstraint: NSLayoutConstraint!
override func awakeFromNib() {

View File

@ -34,7 +34,7 @@ class AuthController: UIViewController {
IHProgressHUD.show()
Api.login(username: name, password: pass)
.observeOn(MainScheduler.instance)
.subscribe(onNext: self.goToMainScreen(user:), onError: self.displayError(error:))
.subscribe(onSuccess: self.goToMainScreen(user:), onError: self.displayError(error:))
.disposed(by: self.bag)
}
@ -44,7 +44,7 @@ class AuthController: UIViewController {
IHProgressHUD.show()
Api.signup(username: name, password: pass)
.observeOn(MainScheduler.instance)
.subscribe(onNext: self.goToMainScreen(user:), onError: self.displayError(error:))
.subscribe(onSuccess: self.goToMainScreen(user:), onError: self.displayError(error:))
.disposed(by: self.bag)
}

View File

@ -15,11 +15,14 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener, UITabl
let bag = DisposeBag()
let maskFieldDelegate = MaskedTextFieldDelegate()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
guard let realm = try? Realm() else { return }
self.hideKeyboardWhenTappedAround()
self.maskFieldDelegate.primaryMaskFormat = "[A][000][AA] [009]"
self.maskFieldDelegate.listener = self
self.number.delegate = self.maskFieldDelegate
@ -70,6 +73,8 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener, UITabl
self.handleQuickActions()
}
// MARK: -
func handleQuickActions() {
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
@ -78,9 +83,9 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener, UITabl
ad.quickAction = .none
self.number.becomeFirstResponder()
break
case .checkNumber(let number):
case .checkNumber(let number, let event):
ad.quickAction = .none
self.check(number: number)
self.check(number: number, event: event)
break
case .addVoiceRecord:
self.tabBarController?.selectedIndex = 1
@ -90,21 +95,25 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener, UITabl
}
}
// MARK: - Checking new number
@IBAction func checkTapped(_ sender: UIButton) {
guard let number = self.number.text else { return }
let numberNormalized = number.filter { !$0.isWhitespace }.uppercased()
self.check(number: numberNormalized)
self.check(number: numberNormalized, event: nil)
}
func check(number: String) {
func check(number: String, event: VehicleEvent?) {
self.number.resignFirstResponder()
self.number.text = nil
self.check.isEnabled = false
IHProgressHUD.show()
Api.checkVehicle(by: number)
.observeOn(MainScheduler.instance)
.subscribe(onNext: onReceivedVehicle(_:), onError: { err in
.subscribe(onSuccess: { vehicle in
self.onReceivedVehicle(vehicle, event: event)
}, onError: { err in
if let realm = try? Realm() {
try? realm.write {
realm.add(Vehicle(number), update: .all)
@ -115,23 +124,32 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener, UITabl
}).disposed(by: self.bag)
}
func textField(_ textField: UITextField, didFillMandatoryCharacters complete: Bool, didExtractValue value: String) {
self.check.isEnabled = complete
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if self.check.isEnabled {
self.checkTapped(self.check)
}
return true
}
func onReceivedVehicle(_ vehicle: Vehicle) {
func save(vehicle: Vehicle) {
if let realm = try? Realm() {
try? realm.write {
realm.add(vehicle, update: .all)
}
}
}
func getEvent() -> Single<VehicleEvent> {
if let event = LocationManager.lastEvent, (Date().timeIntervalSince1970 - event.date) < 30 {
print("Using last event")
return Single<VehicleEvent>.just(event)
} else {
print("requesting new event")
return LocationManager.requestCurrentLocation()
}
}
func onReceivedVehicle(_ vehicle: Vehicle, event: VehicleEvent? = nil) {
self.save(vehicle: vehicle)
let eventSingle = event == nil ? self.getEvent() : Single.just(event!)
eventSingle
.flatMap { Api.add(event: $0, to: vehicle.number) }
.subscribe(onSuccess: self.save(vehicle:), onError: { print("Error adding event: \($0)") })
.disposed(by: self.bag)
self.updateDetailController(with: vehicle)
IHProgressHUD.dismiss()
@ -158,6 +176,25 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener, UITabl
}
}
// MARK: - UITextFieldDelegate
func textField(_ textField: UITextField, didFillMandatoryCharacters complete: Bool, didExtractValue value: String) {
self.check.isEnabled = complete
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if self.check.isEnabled {
self.checkTapped(self.check)
}
return true
}
func textFieldDidBeginEditing(_ textField: UITextField) {
LocationManager.requestCurrentLocation().subscribe().disposed(by: self.bag)
}
// MARK: -
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
guard let vehicle: Vehicle = try? self.history.rx.model(at: indexPath) else { return nil }
@ -165,7 +202,9 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener, UITabl
IHProgressHUD.show()
Api.checkVehicle(by: vehicle.number, force: true)
.observeOn(MainScheduler.instance)
.subscribe(onNext: self.onReceivedVehicle(_:), onError: { err in
.subscribe(onSuccess: { vehicle in
self.onReceivedVehicle(vehicle, event: nil)
}, onError: { err in
IHProgressHUD.showError(withStatus: err.localizedDescription)
print(err.localizedDescription)
}).disposed(by: self.bag)

View File

@ -20,7 +20,7 @@ class FiltersController: FormViewController {
row.value = self.filter.brand ?? "Any"
row.selectorTitle = "Brands"
row.optionsProvider = .lazy({ form, completion in
Api.getBrands().observeOn(MainScheduler.instance).subscribe(onNext: { brands in
Api.getBrands().observeOn(MainScheduler.instance).subscribe(onSuccess: { brands in
completion(["Any"] + brands)
}, onError: { error in
print("Get brands error: ", error)
@ -39,7 +39,7 @@ class FiltersController: FormViewController {
completion(["Any"])
return
}
Api.getModels(of: brand).observeOn(MainScheduler.instance).subscribe(onNext: { models in
Api.getModels(of: brand).observeOn(MainScheduler.instance).subscribe(onSuccess: { models in
completion(["Any"] + models)
}, onError: { error in
print("Get models error: ", error)
@ -53,7 +53,7 @@ class FiltersController: FormViewController {
row.title = "Color"
row.value = self.filter.color ?? "Any"
row.optionsProvider = .lazy({ form, completion in
Api.getColors().observeOn(MainScheduler.instance).subscribe(onNext: { colors in
Api.getColors().observeOn(MainScheduler.instance).subscribe(onSuccess: { colors in
completion(["Any"] + colors)
}, onError: { error in
print("Get colors error: ", error)

View File

@ -123,10 +123,12 @@ class RecordsController: UIViewController, UITableViewDelegate {
.observeOn(MainScheduler.instance)
.flatMap(self.makeStartSoundIfNeeded)
.flatMap {
DispatchQueue.main.async {
alert = UIAlertController(title: "Recording...", message: nil, preferredStyle: .alert)
alert!.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in self.recordDisposable?.dispose() }))
alert!.addAction(UIAlertAction(title: "Done", style: .default, handler: { _ in self.recorder?.stopRecording() }))
self.present(alert!, animated: true)
}
let date = Date()
let fileName = "recording-\(date.timeIntervalSince1970).m4a"
@ -141,13 +143,20 @@ class RecordsController: UIViewController, UITableViewDelegate {
return AudioRecord(path: url.lastPathComponent, number: self.getPlateNumber(from: text), raw: text, duration: duration, event: event)
}
.subscribe(onSuccess: { record in
print(record)
let realm = try? Realm()
try? realm?.write {
realm?.add(record)
}
alert?.dismiss(animated: true)
}) { error in
IHProgressHUD.showError(withStatus: error.localizedDescription)
if let alert = alert {
alert.dismiss(animated: true) {
IHProgressHUD.show(error: error)
}
} else {
IHProgressHUD.show(error: error)
}
}
}
@ -221,7 +230,7 @@ class RecordsController: UIViewController, UITableViewDelegate {
let check = UIContextualAction(style: .normal, title: "Check") { action, view, completion in
if let number = record.number {
self.check(number: number)
self.check(number: number, event: record.event)
}
completion(true)
}
@ -276,9 +285,9 @@ class RecordsController: UIViewController, UITableViewDelegate {
self.present(sheet, animated: true, completion: nil)
}
func check(number: String) {
func check(number: String, event: VehicleEvent?) {
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
ad.quickAction = .checkNumber(number)
ad.quickAction = .checkNumber(number, event)
self.tabBarController?.selectedIndex = 0
}

View File

@ -46,7 +46,7 @@ class RegionsController: UIViewController, UISearchResultsUpdating, UITableViewD
return cell
}
Api.getRegions().observeOn(MainScheduler.instance).subscribe(onNext: { regions in
Api.getRegions().observeOn(MainScheduler.instance).subscribe(onSuccess: { regions in
self.regions = regions
self.regionsFiltered = regions
self.updateTableView()

View File

@ -1,22 +1,44 @@
import UIKit
extension NSError {
var displayMessage: (title: String, body: String) {
if let description = self.userInfo[NSLocalizedDescriptionKey] as? String {
return (title: "Error", body: description)
} else if let failure = self.userInfo[NSLocalizedFailureErrorKey] as? String, let reason = self.localizedFailureReason {
if let recovery = self.localizedRecoverySuggestion {
return (title: failure, body: reason + "\n" + recovery)
} else {
return (title: failure, body: reason)
}
} else {
return (title: "Error", body: "")
}
}
}
extension CocoaError {
static func error(_ description: String) -> NSError {
return error(Code(rawValue: 0), userInfo: [NSLocalizedDescriptionKey: description], url: nil) as NSError
}
static func error(_ description: String, suggestion: String) -> NSError {
let info = [
NSLocalizedDescriptionKey: description,
NSLocalizedRecoverySuggestionErrorKey: suggestion
static func error(_ error: String, reason: String, suggestion: String? = nil) -> NSError {
var info = [
NSLocalizedFailureErrorKey: error,
NSLocalizedFailureReasonErrorKey: reason
]
return error(Code(rawValue: 0), userInfo: info, url: nil) as NSError
if let suggestion = suggestion {
info[NSLocalizedRecoverySuggestionErrorKey] = suggestion
}
return self.error(Code(rawValue: 0), userInfo: info, url: nil) as NSError
}
}
extension UIViewController {
func show(error: NSError) {
let alert = UIAlertController(title: error.localizedDescription, message: error.localizedRecoverySuggestion, preferredStyle: .alert)
func show(error: Error) {
let msg = (error as NSError).displayMessage
let alert = UIAlertController(title: msg.title, message: msg.body, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true)
}
@ -27,3 +49,10 @@ extension UIViewController {
self.present(alert, animated: true)
}
}
extension IHProgressHUD {
static func show(error: Error) {
let msg = (error as NSError).displayMessage
self.showError(withStatus: msg.title + "\n" + msg.body)
}
}

View File

@ -0,0 +1,13 @@
import UIKit
extension UIViewController {
func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
@objc func dismissKeyboard() {
view.endEditing(true)
}
}

View File

@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Access is needed for storing locations of vehicles</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
@ -44,6 +42,8 @@
</dict>
</dict>
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Access is needed for storing locations of vehicles</string>
<key>NSMicrophoneUsageDescription</key>
<string>Access is needed for voice recordings</string>
<key>NSPhotoLibraryAddUsageDescription</key>

View File

@ -70,6 +70,7 @@ class Vehicle: Object, Decodable, IdentifiableType {
@objc dynamic var addedBy: String = ""
let photos = List<VehiclePhoto>()
let ownershipPeriods = List<VehicleOwnershipPeriod>()
let events = List<VehicleEvent>()
var identity: String { number }
@ -91,6 +92,7 @@ class Vehicle: Object, Decodable, IdentifiableType {
case addedBy
case photos
case ownershipPeriods
case events
}
required init(from decoder: Decoder) throws {
@ -98,7 +100,7 @@ class Vehicle: Object, Decodable, IdentifiableType {
self.brand = try container.decodeIfPresent(VehicleBrand.self, forKey: .brand)
self.model = try container.decodeIfPresent(VehicleModel.self, forKey: .model)
self.color = try container.decodeIfPresent(String.self, forKey: .color)
self.year = try container.decode(Int.self, forKey: .year)
self.year = try container.decodeIfPresent(Int.self, forKey: .year) ?? 0
self.category = try container.decodeIfPresent(String.self, forKey: .category)
self.engine = try container.decodeIfPresent(VehicleEngine.self, forKey: .engine)
self.number = try container.decode(String.self, forKey: .number)
@ -118,6 +120,10 @@ class Vehicle: Object, Decodable, IdentifiableType {
if let ownersipsArray = try container.decodeIfPresent([VehicleOwnershipPeriod].self, forKey: .ownershipPeriods) {
self.ownershipPeriods.append(objectsIn: ownersipsArray)
}
if let eventsArray = try container.decodeIfPresent([VehicleEvent].self, forKey: .events) {
self.events.append(objectsIn: eventsArray)
}
}
required init() {

View File

@ -1,12 +1,13 @@
import Foundation
import RealmSwift
class VehicleEvent: Object {
@objc dynamic var date: Date = Date()
class VehicleEvent: Object, Codable {
@objc dynamic var date: TimeInterval = Date().timeIntervalSince1970
@objc dynamic var latitude: Double = 0
@objc dynamic var longitude: Double = 0
@objc dynamic var speed: Double = 0
@objc dynamic var direction: Double = 0
@objc dynamic var address: String? = nil
init(lat: Double, lon: Double, speed: Double, dir: Double) {
self.latitude = lat
@ -18,4 +19,8 @@ class VehicleEvent: Object {
required init() {
super.init()
}
override class func ignoredProperties() -> [String] {
return ["address"]
}
}

273
AutoCat/ThirdParty/AnyEncodable.swift vendored Normal file
View File

@ -0,0 +1,273 @@
#if canImport(Foundation)
import Foundation
#endif
/**
A type-erased `Encodable` value.
The `AnyEncodable` type forwards encoding responsibilities
to an underlying value, hiding its specific underlying type.
You can encode mixed-type values in dictionaries
and other collections that require `Encodable` conformance
by declaring their contained type to be `AnyEncodable`:
let dictionary: [String: AnyEncodable] = [
"boolean": true,
"integer": 1,
"double": 3.141592653589793,
"string": "string",
"array": [1, 2, 3],
"nested": [
"a": "alpha",
"b": "bravo",
"c": "charlie"
]
]
let encoder = JSONEncoder()
let json = try! encoder.encode(dictionary)
*/
#if swift(>=5.1)
@frozen public struct AnyEncodable: Encodable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
}
#else
public struct AnyEncodable: Encodable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
}
#endif
#if swift(>=4.2)
@usableFromInline
protocol _AnyEncodable {
var value: Any { get }
init<T>(_ value: T?)
}
#else
protocol _AnyEncodable {
var value: Any { get }
init<T>(_ value: T?)
}
#endif
extension AnyEncodable: _AnyEncodable {}
// MARK: - Encodable
extension _AnyEncodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch value {
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
case let number as NSNumber:
try encode(nsnumber: number, into: &container)
#endif
#if canImport(Foundation)
case is NSNull:
try container.encodeNil()
#endif
case is Void:
try container.encodeNil()
case let bool as Bool:
try container.encode(bool)
case let int as Int:
try container.encode(int)
case let int8 as Int8:
try container.encode(int8)
case let int16 as Int16:
try container.encode(int16)
case let int32 as Int32:
try container.encode(int32)
case let int64 as Int64:
try container.encode(int64)
case let uint as UInt:
try container.encode(uint)
case let uint8 as UInt8:
try container.encode(uint8)
case let uint16 as UInt16:
try container.encode(uint16)
case let uint32 as UInt32:
try container.encode(uint32)
case let uint64 as UInt64:
try container.encode(uint64)
case let float as Float:
try container.encode(float)
case let double as Double:
try container.encode(double)
case let string as String:
try container.encode(string)
#if canImport(Foundation)
case let date as Date:
try container.encode(date)
case let url as URL:
try container.encode(url)
#endif
case let array as [Any?]:
try container.encode(array.map { AnyEncodable($0) })
case let dictionary as [String: Any?]:
try container.encode(dictionary.mapValues { AnyEncodable($0) })
case let enc as Encodable:
//try container.encode(enc)
try enc.encode(to: encoder)
default:
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "AnyEncodable value cannot be encoded")
throw EncodingError.invalidValue(value, context)
}
}
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws {
switch CFNumberGetType(nsnumber) {
case .charType:
try container.encode(nsnumber.boolValue)
case .sInt8Type:
try container.encode(nsnumber.int8Value)
case .sInt16Type:
try container.encode(nsnumber.int16Value)
case .sInt32Type:
try container.encode(nsnumber.int32Value)
case .sInt64Type:
try container.encode(nsnumber.int64Value)
case .shortType:
try container.encode(nsnumber.uint16Value)
case .longType:
try container.encode(nsnumber.uint32Value)
case .longLongType:
try container.encode(nsnumber.uint64Value)
case .intType, .nsIntegerType, .cfIndexType:
try container.encode(nsnumber.intValue)
case .floatType, .float32Type:
try container.encode(nsnumber.floatValue)
case .doubleType, .float64Type, .cgFloatType:
try container.encode(nsnumber.doubleValue)
#if swift(>=5.0)
@unknown default:
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "NSNumber cannot be encoded because its type is not handled")
throw EncodingError.invalidValue(nsnumber, context)
#endif
}
}
#endif
}
extension AnyEncodable: Equatable {
public static func == (lhs: AnyEncodable, rhs: AnyEncodable) -> Bool {
switch (lhs.value, rhs.value) {
case is (Void, Void):
return true
case let (lhs as Bool, rhs as Bool):
return lhs == rhs
case let (lhs as Int, rhs as Int):
return lhs == rhs
case let (lhs as Int8, rhs as Int8):
return lhs == rhs
case let (lhs as Int16, rhs as Int16):
return lhs == rhs
case let (lhs as Int32, rhs as Int32):
return lhs == rhs
case let (lhs as Int64, rhs as Int64):
return lhs == rhs
case let (lhs as UInt, rhs as UInt):
return lhs == rhs
case let (lhs as UInt8, rhs as UInt8):
return lhs == rhs
case let (lhs as UInt16, rhs as UInt16):
return lhs == rhs
case let (lhs as UInt32, rhs as UInt32):
return lhs == rhs
case let (lhs as UInt64, rhs as UInt64):
return lhs == rhs
case let (lhs as Float, rhs as Float):
return lhs == rhs
case let (lhs as Double, rhs as Double):
return lhs == rhs
case let (lhs as String, rhs as String):
return lhs == rhs
case let (lhs as [String: AnyEncodable], rhs as [String: AnyEncodable]):
return lhs == rhs
case let (lhs as [AnyEncodable], rhs as [AnyEncodable]):
return lhs == rhs
default:
return false
}
}
}
extension AnyEncodable: CustomStringConvertible {
public var description: String {
switch value {
case is Void:
return String(describing: nil as Any?)
case let value as CustomStringConvertible:
return value.description
default:
return String(describing: value)
}
}
}
extension AnyEncodable: CustomDebugStringConvertible {
public var debugDescription: String {
switch value {
case let value as CustomDebugStringConvertible:
return "AnyEncodable(\(value.debugDescription))"
default:
return "AnyEncodable(\(description))"
}
}
}
extension AnyEncodable: ExpressibleByNilLiteral {}
extension AnyEncodable: ExpressibleByBooleanLiteral {}
extension AnyEncodable: ExpressibleByIntegerLiteral {}
extension AnyEncodable: ExpressibleByFloatLiteral {}
extension AnyEncodable: ExpressibleByStringLiteral {}
#if swift(>=5.0)
extension AnyEncodable: ExpressibleByStringInterpolation {}
#endif
extension AnyEncodable: ExpressibleByArrayLiteral {}
extension AnyEncodable: ExpressibleByDictionaryLiteral {}
extension _AnyEncodable {
public init(nilLiteral _: ()) {
self.init(nil as Any?)
}
public init(booleanLiteral value: Bool) {
self.init(value)
}
public init(integerLiteral value: Int) {
self.init(value)
}
public init(floatLiteral value: Double) {
self.init(value)
}
public init(extendedGraphemeClusterLiteral value: String) {
self.init(value)
}
public init(stringLiteral value: String) {
self.init(value)
}
public init(arrayLiteral elements: Any...) {
self.init(elements)
}
public init(dictionaryLiteral elements: (AnyHashable, Any)...) {
self.init([AnyHashable: Any](elements, uniquingKeysWith: { first, _ in first }))
}
}

View File

@ -6,11 +6,11 @@ class Api {
return NSError(domain: "", code: code, userInfo: [NSLocalizedDescriptionKey: msg, NSLocalizedRecoverySuggestionErrorKey: suggestion])
}
private static func createRequest(api: String, method: String, body: [String: String]? = nil) -> URLRequest? {
private static func createRequest<B,P>(api: String, method: String, body: B? = nil, params: [String:P]? = nil) -> URLRequest? where B: Encodable, P: LosslessStringConvertible {
guard var urlComponents = URLComponents(string: Constants.baseUrl + api) else { return nil }
if let body = body, method.uppercased() == "GET" {
urlComponents.queryItems = body.map { URLQueryItem(name: $0, value: String($1)) }
if let params = params, method.uppercased() == "GET" {
urlComponents.queryItems = params.map { URLQueryItem(name: $0, value: String($1)) }
}
var request = URLRequest(url: urlComponents.url!)
@ -20,21 +20,29 @@ class Api {
request.addValue("Bearer " + Settings.shared.user.token, forHTTPHeaderField: "Authorization")
if let body = body, method.uppercased() != "GET" {
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let data = try? encoder.encode(body) {
request.httpBody = data
}
}
return request
}
private static func makeRequest<T>(api: String, method: String = "GET", body: [String: String]? = nil) -> Observable<T> where T: Decodable {
guard let request = self.createRequest(api: api, method: method, body: body) else {
return Observable.error(self.genError("Error creating request", suggestion: ""))
private static func makeRequest<T,B,P>(api: String, method: String = "GET", body: B?, params: [String:P]? = nil) -> Single<T> where T: Decodable, B: Encodable, P: LosslessStringConvertible {
guard let request = self.createRequest(api: api, method: method, body: body, params: params) else {
return Single.error(self.genError("Error creating request", suggestion: ""))
}
return URLSession.shared.rx.data(request: request).map { data in
return URLSession.shared.rx.data(request: request).asSingle().map { data in
// let str = String(data: data, encoding: .utf8)
// print("================================")
// print(str?.replacingOccurrences(of: "\\\"", with: "\""))
// if let string = str?.replacingOccurrences(of: "\\\"", with: "\"")
// .replacingOccurrences(of: "\\'", with: "'")
// .replacingOccurrences(of: "\\n", with: "") {
// print(string)
// }
// print("================================")
let resp = try JSONDecoder().decode(Response<T>.self, from: data)
if resp.success {
@ -45,7 +53,27 @@ class Api {
}
}
public static func refreshFbToken() -> Observable<Void> {
private static func makeGetRequest<T,P>(api: String, params:[String: P]? = nil) -> Single<T> where T: Decodable, P: LosslessStringConvertible {
// Kind of hack to satisfy compiler
return self.makeRequest(api: api, method: "GET", body: nil as Int?, params: params)
}
private static func makeEmptyGetRequest<T>(api: String) -> Single<T> where T: Decodable {
// Same hack as before
return self.makeRequest(api: api, method: "GET", body: nil as Int?, params: nil as [String:Int]?)
}
private static func makeEmptyBodyRequest<T>(api: String, method: String = "POST") -> Single<T> where T: Decodable {
// Same hack as before
return self.makeRequest(api: api, method: method, body: nil as Int?, params: nil as [String:Int]?)
}
private static func makeBodyRequest<T,B>(api: String, body: B?, method: String = "POST") -> Single<T> where T: Decodable, B: Encodable {
// Same hack as before
return self.makeRequest(api: api, method: method, body: body, params: nil as [String:Int]?)
}
public static func refreshFbToken() -> Single<Void> {
guard let token = Settings.shared.user.googleIdToken, let refreshToken = Settings.shared.user.googleRefreshToken, let jwt = JWT(string: token), jwt.expired else {
return .just(())
}
@ -65,7 +93,7 @@ class Api {
request.addValue(Constants.fbClientVersion, forHTTPHeaderField: "X-Client-Version")
request.addValue(Constants.vin01BUndleId, forHTTPHeaderField: "X-Ios-Bundle-Identifier")
request.addValue(Constants.fbUserAgent, forHTTPHeaderField: "User-Agent")
return URLSession.shared.rx.json(request: request).map { resp in
return URLSession.shared.rx.json(request: request).asSingle().map { resp in
guard let json = resp as? [String: Any] else { return }
if let newToken = json["id_token"] as? String {
Settings.shared.user.googleIdToken = newToken
@ -83,28 +111,28 @@ class Api {
}
}
public static func login(username: String, password: String) -> Observable<User> {
public static func login(username: String, password: String) -> Single<User> {
let body = [
"login": username,
"password": password
]
return self.makeRequest(api: "user/login", method: "POST", body: body)
return self.makeBodyRequest(api: "user/login", body: body)
}
public static func signup(username: String, password: String) -> Observable<User> {
public static func signup(username: String, password: String) -> Single<User> {
let body = [
"login": username,
"password": password
]
return self.makeRequest(api: "user/signup", method: "POST", body: body)
return self.makeBodyRequest(api: "user/signup", body: body)
}
public static func getVehicles(with filter: Filter) -> Observable<[Vehicle]> {
return self.makeRequest(api: "vehicles", method: "GET", body: filter.queryDictionary())
public static func getVehicles(with filter: Filter) -> Single<[Vehicle]> {
return self.makeGetRequest(api: "vehicles", params: filter.queryDictionary())
}
public static func checkVehicle(by number: String, force: Bool = false) -> Observable<Vehicle> {
return self.refreshFbToken().flatMap { () -> Observable<Vehicle> in
public static func checkVehicle(by number: String, force: Bool = false) -> Single<Vehicle> {
return self.refreshFbToken().flatMap { () -> Single<Vehicle> in
var body = [
"number": number,
"forceUpdate": String(force)
@ -112,26 +140,34 @@ class Api {
if let token = Settings.shared.user.googleIdToken {
body["googleIdToken"] = token
}
return self.makeRequest(api: "vehicles/check", method: "POST", body: body).map { (vehicle: Vehicle) -> Vehicle in
return self.makeBodyRequest(api: "vehicles/check", body: body).map { (vehicle: Vehicle) -> Vehicle in
vehicle.addedDate = Date().timeIntervalSince1970*1000
return vehicle
}
}
}
public static func getBrands() -> Observable<[String]> {
return self.makeRequest(api: "vehicles/brands")
public static func getBrands() -> Single<[String]> {
return self.makeEmptyGetRequest(api: "vehicles/brands")
}
public static func getModels(of brand: String) -> Observable<[String]> {
return self.makeRequest(api: "vehicles/models", body: ["brand": brand])
public static func getModels(of brand: String) -> Single<[String]> {
return self.makeGetRequest(api: "vehicles/models", params: ["brand": brand])
}
public static func getColors() -> Observable<[String]> {
return self.makeRequest(api: "vehicles/colors")
public static func getColors() -> Single<[String]> {
return self.makeEmptyGetRequest(api: "vehicles/colors")
}
public static func getRegions() -> Observable<[Region]> {
return self.makeRequest(api: "vehicles/regions")
public static func getRegions() -> Single<[Region]> {
return self.makeEmptyGetRequest(api: "vehicles/regions")
}
public static func add(event: VehicleEvent, to number: String) -> Single<Vehicle> {
let body = ["number": AnyEncodable(number), "event": AnyEncodable(event)]
return self.makeBodyRequest(api: "events", body: body).map { (vehicle: Vehicle) -> Vehicle in
vehicle.addedDate = Date().timeIntervalSince1970*1000
return vehicle
}
}
}

View File

@ -3,7 +3,7 @@ import Foundation
enum Constants {
static var baseUrl: String {
#if DEBUG
//return "http://127.0.0.1:3000/"
//return "http://192.168.1.67:3000/"
return "https://vps.aliencat.pro:8443/"
#else
return "https://vps.aliencat.pro:8443/"

View File

@ -107,7 +107,7 @@ class JWT {
let bodyData = try JSONSerialization.data(withJSONObject: bodyDict, options: [])
guard let body = String(data: bodyData, encoding: .utf8) else {
throw CocoaError.error("Error", suggestion: "Error generating JWT for sharing report via link")
throw CocoaError.error("Error", reason: "Failed to generate JWT for sharing report via link")
}
let twoParts = Base64FS.encodeString(str: header).trimmingCharacters(in: CharacterSet(charactersIn: "=")) + "." + Base64FS.encodeString(str: body).trimmingCharacters(in: CharacterSet(charactersIn: "="))
let signature = twoParts.hmac(algorithm: .SHA256, key: Constants.reportLinkTokenSecret)

View File

@ -5,8 +5,11 @@ import CoreLocation
class RxLocationManagerDelegateProxy: DelegateProxy<CLLocationManager, CLLocationManagerDelegate>, DelegateProxyType, CLLocationManagerDelegate {
let authSubject = PublishSubject<CLAuthorizationStatus>()
let locationSubject = PublishSubject<CLLocation>()
private let generalErrors: [CLError.Code] = [.locationUnknown, .denied, .network, .headingFailure, .rangingUnavailable, .rangingFailure]
private let geocodingErrors: [CLError.Code] = [.geocodeCanceled, .geocodeFoundNoResult, .geocodeFoundPartialResult]
private(set) var authSubject = PublishSubject<CLAuthorizationStatus>()
private(set) var locationSubject = PublishSubject<CLLocation>()
init(locationManager: ParentObject) {
super.init(parentObject: locationManager, delegateProxy: RxLocationManagerDelegateProxy.self)
@ -16,6 +19,17 @@ class RxLocationManagerDelegateProxy: DelegateProxy<CLLocationManager, CLLocatio
print("deinit")
}
// func generateAuthObservable() -> Observable<CLAuthorizationStatus> {
// self.authSubject = PublishSubject<CLAuthorizationStatus>()
// return self.authSubject
// }
//
// func generateLocationObservable() -> Observable<CLLocation> {
// print("generateLocationObservable")
// self.locationSubject = PublishSubject<CLLocation>()
// return self.locationSubject
// }
// MARK: - DelegateProxyType
static func registerKnownImplementations() {
@ -43,39 +57,19 @@ class RxLocationManagerDelegateProxy: DelegateProxy<CLLocationManager, CLLocatio
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
}
}
guard let err = error as? CLError else { return }
/*
extension Reactive where Base: CLLocationManager {
var delegate: DelegateProxy<CLLocationManager, CLLocationManagerDelegate> {
return RxLocationManagerDelegateProxy.proxy(for: base)
}
var didChangeAuthorization: ControlEvent<CLAuthorizationStatus> {
let sel = #selector((CLLocationManagerDelegate.locationManager(_:didChangeAuthorization:)! as (CLLocationManagerDelegate) -> (CLLocationManager, CLAuthorizationStatus) -> Void))
let source: Observable<CLAuthorizationStatus> = delegate.methodInvoked(sel)
.map { arg in
let status = CLAuthorizationStatus(rawValue: arg[1] as! Int32)
return status!
}
return ControlEvent(events: source)
}
var didUpdateLocations: Observable<VehicleEvent> {
let sel = #selector((CLLocationManagerDelegate.locationManager(_:didUpdateLocations:)! as (CLLocationManagerDelegate) -> (CLLocationManager, [CLLocation]) -> Void))
return delegate.methodInvoked(sel)
.map { args in
if let locations = args[1] as? [CLLocation], let location = locations.first {
return VehicleEvent(lat: location.coordinate.latitude, lon: location.coordinate.longitude, speed: location.speed, dir: location.course)
if self.generalErrors.contains(err.code) {
// Pass general errors to all existing subjects
self.authSubject.onError(error)
self.locationSubject.onError(error)
} else if self.geocodingErrors.contains(err.code) {
// TODO: pass error to geocoding subject
} else {
throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Update location error"])
}
print("Unexpected CoreLocation error: \(error)")
}
}
}
*/
class LocationManager {
private static let manager: CLLocationManager = {
@ -84,6 +78,7 @@ class LocationManager {
return mgr
}()
private static let bag = DisposeBag()
private(set) static var lastEvent: VehicleEvent?
private static func checkPermissions() -> Single<Void> {
return Single<Void>.create { observer in
@ -93,7 +88,8 @@ class LocationManager {
break
case .notDetermined:
self.manager.requestWhenInUseAuthorization()
_ = RxLocationManagerDelegateProxy.proxy(for: self.manager).authSubject.skip(1).first().subscribe(onSuccess: { result in
let proxy = RxLocationManagerDelegateProxy.proxy(for: self.manager)
_ = proxy.authSubject.skip(1).first().subscribe(onSuccess: { result in
if let status = result, status == .authorizedWhenInUse {
observer(.success(()))
} else {
@ -110,14 +106,24 @@ class LocationManager {
}
private static func requestLocation() -> Single<VehicleEvent> {
let single = RxLocationManagerDelegateProxy.proxy(for: self.manager).locationSubject.take(1).asSingle().map { location in
return VehicleEvent(lat: location.coordinate.latitude, lon: location.coordinate.longitude, speed: location.speed, dir: location.course)
let proxy = RxLocationManagerDelegateProxy.proxy(for: self.manager)
let single = proxy.locationSubject.take(1).asSingle().map { location -> VehicleEvent in
let event = VehicleEvent(lat: location.coordinate.latitude, lon: location.coordinate.longitude, speed: location.speed, dir: location.course)
self.lastEvent = event
return event
}
self.manager.requestLocation()
return single
}
static func requestCurrentLocation() -> Single<VehicleEvent> {
return self.checkPermissions().flatMap(self.requestLocation)
return self.checkPermissions().flatMap(self.requestLocation).do(onSuccess: { event in
print("Get location success")
}, onSubscribed: {
print("Get location subscribed")
}, onDispose: {
print("Get location dispose")
self.manager.stopUpdatingLocation()
})
}
}

View File

@ -26,6 +26,19 @@ class Recorder {
init() {
}
func microphoneAvailable() -> Bool {
// FIXME:
// This is primarily for mac catalyst app.
// On iOS this will always return true (as there is always at least one microphone on any iOS device)
let session = AVAudioSession.sharedInstance()
// #if targetEnvironment(macCatalyst)
// for input in session.availableInputs! {
// print(input.portType == .headsetMic)
// }
// #endif
return session.availableInputs?.contains(where: { $0.portType == .builtInMic }) ?? false
}
func requestPermissions() -> Single<Void> {
return Single<Void>.create { observer in
AVAudioSession.sharedInstance().requestRecordPermission { allowed in
@ -36,25 +49,25 @@ class Recorder {
observer(.success(()))
break
case .denied:
let error = CocoaError.error("Access denied", suggestion: "Please give permission to use speech recognition in system settings")
let error = CocoaError.error("Access error", reason: "Access to speech recognition is denied", suggestion: "Please give permission to use speech recognition in system settings")
observer(.error(error))
break
case .restricted:
let error = CocoaError.error("Access restricted", suggestion: "Speech recognition is restricted on this device")
let error = CocoaError.error("Access error", reason: "Speech recognition is restricted on this device")
observer(.error(error))
break
case .notDetermined:
let error = CocoaError.error("Access error", suggestion: "Speech recognition status is not yet determined")
let error = CocoaError.error("Access error", reason: "Speech recognition status is not yet determined")
observer(.error(error))
break
@unknown default:
let error = CocoaError.error("Access error", suggestion: "Unknown error accessing speech recognizer")
let error = CocoaError.error("Access error", reason: "Unknown error accessing speech recognizer")
observer(.error(error))
break
}
}
} else {
let error = CocoaError.error("Access denied", suggestion: "Please give permission to use microphone in system settings")
let error = CocoaError.error("Access error", reason: "Access to microphone is denied", suggestion: "Please give permission to use microphone in system settings")
observer(.error(error))
}
}
@ -64,9 +77,13 @@ class Recorder {
}
func startRecording(to file: URL) -> Single<String> {
guard self.microphoneAvailable() else {
return Single.error(CocoaError.error("Recording error", reason: "Microphone not found"))
}
return Single<String>.create { observer in
guard let aac = AVAudioFormat(settings: self.recordingSettings) else {
observer(.error(CocoaError.error("Recording error", suggestion: "Format not supported")))
observer(.error(CocoaError.error("Recording error", reason: "Format not supported")))
return Disposables.create()
}

View File

@ -1,16 +1,32 @@
import UIKit
class CustomTextField: UITextField {
class CustomTextField: UITextField, UITextFieldDelegate {
@IBInspectable var editable: Bool = true
@IBInspectable var borderWidth: CGFloat = 1 {
didSet {
self.layer.borderWidth = self.borderWidth
}
}
@IBInspectable var cornerRadius: CGFloat = 6 {
didSet {
self.layer.cornerRadius = self.cornerRadius
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
//self.borderStyle = .none
self.layer.borderWidth = 1
self.layer.cornerRadius = 6
self.delegate = self
}
override func layoutSubviews() {
super.layoutSubviews()
self.layer.borderColor = UIColor.secondaryLabel.cgColor
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return self.editable
}
}