New project
This commit is contained in:
parent
1752d89b78
commit
d023c60f54
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
AutoCat2.xcodeproj/xcuserdata/
|
AutoCat2.xcodeproj/xcuserdata/
|
||||||
|
AutoCat2.xcodeproj/project.xcworkspace/xcuserdata/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.app-sandbox</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.network.client</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "pkhud",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pkluz/PKHUD.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "8fd26f23057c6bebd6695524b1c3e05e93aba571",
|
||||||
|
"version" : "5.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftentrykit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/huri000/SwiftEntryKit",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "5ad36cccf0c4b9fea32f4e9b17a8e38f07563ef0",
|
||||||
|
"version" : "2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
@ -4,39 +4,16 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>SchemeUserState</key>
|
<key>SchemeUserState</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>AutoCat2 (iOS).xcscheme_^#shared#^_</key>
|
<key>AutoCat2.xcscheme</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>AutoCat2 (macOS).xcscheme_^#shared#^_</key>
|
<key>AutoCatCore.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>AutoCatCore.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>2</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>SuppressBuildableAutocreation</key>
|
|
||||||
<dict>
|
|
||||||
<key>7A40D5822691C6D8009B0BC4</key>
|
|
||||||
<dict>
|
|
||||||
<key>primary</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>7A40D58D2691C6D8009B0BC4</key>
|
|
||||||
<dict>
|
|
||||||
<key>primary</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>7A40D5F22693A63A009B0BC4</key>
|
|
||||||
<dict>
|
|
||||||
<key>primary</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
36
AutoCat2/AppDelegate.swift
Normal file
36
AutoCat2/AppDelegate.swift
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// AppDelegate.swift
|
||||||
|
// AutoCat2
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 05.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
// Override point for customization after application launch.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: UISceneSession Lifecycle
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||||
|
// Called when a new scene session is being created.
|
||||||
|
// Use this method to select a configuration to create the new scene with.
|
||||||
|
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||||
|
// Called when the user discards a scene session.
|
||||||
|
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||||
|
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -89,56 +89,6 @@
|
|||||||
"idiom" : "ios-marketing",
|
"idiom" : "ios-marketing",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "mac",
|
|
||||||
"scale" : "1x",
|
|
||||||
"size" : "16x16"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "mac",
|
|
||||||
"scale" : "2x",
|
|
||||||
"size" : "16x16"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "mac",
|
|
||||||
"scale" : "1x",
|
|
||||||
"size" : "32x32"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "mac",
|
|
||||||
"scale" : "2x",
|
|
||||||
"size" : "32x32"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "mac",
|
|
||||||
"scale" : "1x",
|
|
||||||
"size" : "128x128"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "mac",
|
|
||||||
"scale" : "2x",
|
|
||||||
"size" : "128x128"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "mac",
|
|
||||||
"scale" : "1x",
|
|
||||||
"size" : "256x256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "mac",
|
|
||||||
"scale" : "2x",
|
|
||||||
"size" : "256x256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "mac",
|
|
||||||
"scale" : "1x",
|
|
||||||
"size" : "512x512"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "mac",
|
|
||||||
"scale" : "2x",
|
|
||||||
"size" : "512x512"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
@ -2,13 +2,8 @@
|
|||||||
"colors" : [
|
"colors" : [
|
||||||
{
|
{
|
||||||
"color" : {
|
"color" : {
|
||||||
"color-space" : "srgb",
|
"platform" : "ios",
|
||||||
"components" : {
|
"reference" : "systemGray2Color"
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.000",
|
|
||||||
"green" : "0.000",
|
|
||||||
"red" : "0.000"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
},
|
},
|
||||||
@ -20,12 +15,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"color" : {
|
"color" : {
|
||||||
"color-space" : "display-p3",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.866",
|
"blue" : "0.290",
|
||||||
"green" : "0.866",
|
"green" : "0.282",
|
||||||
"red" : "0.866"
|
"red" : "0.282"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
@ -20,12 +20,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"color" : {
|
"color" : {
|
||||||
"color-space" : "display-p3",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "0.300",
|
||||||
"blue" : "0.152",
|
"blue" : "1.000",
|
||||||
"green" : "0.152",
|
"green" : "1.000",
|
||||||
"red" : "0.152"
|
"red" : "1.000"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
25
AutoCat2/Base.lproj/LaunchScreen.storyboard
Normal file
25
AutoCat2/Base.lproj/LaunchScreen.storyboard
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
24
AutoCat2/Base.lproj/Main.storyboard
Normal file
24
AutoCat2/Base.lproj/Main.storyboard
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
84
AutoCat2/Components/ACButton.swift
Normal file
84
AutoCat2/Components/ACButton.swift
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 06.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum ACButtonStyle {
|
||||||
|
case generic
|
||||||
|
case roundedBlue
|
||||||
|
}
|
||||||
|
|
||||||
|
class ACButton: UIButton {
|
||||||
|
|
||||||
|
private var style: ACButtonStyle = .generic
|
||||||
|
|
||||||
|
convenience init(style: ACButtonStyle = .roundedBlue, title: String, onTap: @escaping () -> Void) {
|
||||||
|
self.init()
|
||||||
|
self.style(style)
|
||||||
|
self.onTap(onTap)
|
||||||
|
self.title(title)
|
||||||
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(style: ACButtonStyle = .roundedBlue, title: String, onTapAsync: @escaping () async -> Void) {
|
||||||
|
self.init()
|
||||||
|
self.style(style)
|
||||||
|
self.onTapAsync(onTapAsync)
|
||||||
|
self.title(title)
|
||||||
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
if style == .roundedBlue {
|
||||||
|
self.layer.opacity = self.isEnabled ? 1 : 0.5
|
||||||
|
} else {
|
||||||
|
self.layer.opacity = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func style(_ style: ACButtonStyle) -> ACButton {
|
||||||
|
|
||||||
|
self.style = style
|
||||||
|
|
||||||
|
switch style {
|
||||||
|
case .generic:
|
||||||
|
break
|
||||||
|
case .roundedBlue:
|
||||||
|
backgroundColor = .systemBlue
|
||||||
|
layer.cornerRadius = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func title(_ title: String) -> ACButton {
|
||||||
|
|
||||||
|
setTitle(title, for: .normal)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func enable(_ enabled: Bool) -> ACButton {
|
||||||
|
isEnabled = enabled
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func onTap(_ handler: @escaping () -> Void) -> ACButton {
|
||||||
|
|
||||||
|
addAction(for: .touchUpInside, handler)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func onTapAsync(_ handler: @escaping () async -> Void) -> ACButton {
|
||||||
|
|
||||||
|
addActionAsync(for: .touchUpInside, handler)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
}
|
||||||
82
AutoCat2/Components/ACTabBar/ACTabBarButton.swift
Normal file
82
AutoCat2/Components/ACTabBar/ACTabBarButton.swift
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
//
|
||||||
|
// ACTabBarButton.swift
|
||||||
|
// AutoCat2
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 20.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ACTabBarButton: UIView {
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
case normal
|
||||||
|
case selected
|
||||||
|
}
|
||||||
|
|
||||||
|
private var state: State = .normal
|
||||||
|
private var tabBarItem: UITabBarItem?
|
||||||
|
|
||||||
|
// MARK: - Views
|
||||||
|
|
||||||
|
private lazy var imageView: UIImageView = {
|
||||||
|
let view = UIImageView()
|
||||||
|
view.contentMode = .scaleAspectFit
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var titleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = UIFont.preferredFont(forTextStyle: .caption2)
|
||||||
|
label.textColor = tintColor
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var stackView: UIStackView = {
|
||||||
|
let stack = UIStackView(arrangedSubviews: [imageView, titleLabel])
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.spacing = 0
|
||||||
|
stack.distribution = .fill
|
||||||
|
stack.alignment = .center
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(tabBarItem: UITabBarItem) {
|
||||||
|
self.init(frame: .zero)
|
||||||
|
apply(tabBarItem: tabBarItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
addSubview(stackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4),
|
||||||
|
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 4),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
|
||||||
|
imageView.widthAnchor.constraint(equalToConstant: 32),
|
||||||
|
imageView.heightAnchor.constraint(equalToConstant: 32)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(tabBarItem: UITabBarItem) {
|
||||||
|
self.tabBarItem = tabBarItem
|
||||||
|
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
imageView.image = state == .normal ? tabBarItem.image : tabBarItem.selectedImage
|
||||||
|
titleLabel.text = tabBarItem.title
|
||||||
|
}
|
||||||
|
}
|
||||||
67
AutoCat2/Components/ACTabBar/ACTabBarController.swift
Normal file
67
AutoCat2/Components/ACTabBar/ACTabBarController.swift
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
//
|
||||||
|
// ACTabBarController.swift
|
||||||
|
// AutoCat2
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 20.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ACTabBarController: UIViewController {
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
var viewControllers: [UIViewController] = [] {
|
||||||
|
didSet {
|
||||||
|
setupControllers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Views
|
||||||
|
|
||||||
|
private lazy var buttonsStackView: UIStackView = {
|
||||||
|
let stack = UIStackView()
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stack.axis = .horizontal
|
||||||
|
stack.spacing = 0
|
||||||
|
stack.distribution = .fillEqually
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let tabBarView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// tabBarView.backgroundColor = .green
|
||||||
|
// buttonsStackView.backgroundColor = .magenta
|
||||||
|
|
||||||
|
view.addSubview(tabBarView)
|
||||||
|
tabBarView.addSubview(buttonsStackView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tabBarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
tabBarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
tabBarView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
buttonsStackView.leadingAnchor.constraint(equalTo: tabBarView.leadingAnchor),
|
||||||
|
buttonsStackView.trailingAnchor.constraint(equalTo: tabBarView.trailingAnchor),
|
||||||
|
buttonsStackView.topAnchor.constraint(equalTo: tabBarView.topAnchor),
|
||||||
|
buttonsStackView.bottomAnchor.constraint(equalTo: tabBarView.safeAreaLayoutGuide.bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupControllers() {
|
||||||
|
buttonsStackView.removeAllArrangedSubviews()
|
||||||
|
|
||||||
|
for controller in viewControllers {
|
||||||
|
let button = ACTabBarButton(tabBarItem: controller.tabBarItem)
|
||||||
|
buttonsStackView.addArrangedSubview(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
98
AutoCat2/Components/ACTextField.swift
Normal file
98
AutoCat2/Components/ACTextField.swift
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 06.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum ACTextFieldStyle {
|
||||||
|
|
||||||
|
case generic
|
||||||
|
case roundBordered
|
||||||
|
}
|
||||||
|
|
||||||
|
class ACTextField: UITextField, UITextFieldDelegate {
|
||||||
|
|
||||||
|
private var style: ACTextFieldStyle = .generic
|
||||||
|
private var onTextChanged: ((String?) -> Void)?
|
||||||
|
|
||||||
|
var editable: Bool = true
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
convenience init(placeholder: String? = nil, style: ACTextFieldStyle = .roundBordered) {
|
||||||
|
self.init(frame: .zero)
|
||||||
|
|
||||||
|
self.delegate = self
|
||||||
|
self.placeholderText(placeholder)
|
||||||
|
self.style(style)
|
||||||
|
|
||||||
|
addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
|
||||||
|
|
||||||
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
style(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Setters
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func placeholderText(_ placeholder: String?) -> ACTextField {
|
||||||
|
self.placeholder = placeholder
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func keyboardType(_ keyboardType: UIKeyboardType) -> ACTextField {
|
||||||
|
self.keyboardType = keyboardType
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func style(_ style: ACTextFieldStyle) -> ACTextField {
|
||||||
|
|
||||||
|
self.style = style
|
||||||
|
borderStyle = .roundedRect
|
||||||
|
|
||||||
|
switch style {
|
||||||
|
case .generic:
|
||||||
|
layer.borderWidth = 0
|
||||||
|
layer.cornerRadius = 0
|
||||||
|
layer.borderColor = nil
|
||||||
|
break
|
||||||
|
case .roundBordered:
|
||||||
|
layer.borderWidth = 1
|
||||||
|
layer.cornerRadius = 6
|
||||||
|
layer.borderColor = UIColor.secondaryLabel.cgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func secure(_ secure: Bool) -> ACTextField {
|
||||||
|
isSecureTextEntry = secure
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func onTextChanged(_ handler: @escaping (String?) -> Void) -> ACTextField {
|
||||||
|
onTextChanged = handler
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITextFieldDelegate
|
||||||
|
|
||||||
|
func textField(_ textField: UITextField,
|
||||||
|
shouldChangeCharactersIn range: NSRange,
|
||||||
|
replacementString string: String) -> Bool {
|
||||||
|
editable
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func textFieldDidChange(_ textField: UITextField) {
|
||||||
|
onTextChanged?(textField.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
AutoCat2/Components/Extensions/UIControl.swift
Normal file
23
AutoCat2/Components/Extensions/UIControl.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 06.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIControl {
|
||||||
|
|
||||||
|
func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: @escaping () -> Void) {
|
||||||
|
|
||||||
|
addAction(UIAction { _ in closure() }, for: controlEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addActionAsync(for controlEvents: UIControl.Event = .touchUpInside, _ closure: @escaping () async -> Void) {
|
||||||
|
|
||||||
|
addAction(UIAction { _ in
|
||||||
|
Task {
|
||||||
|
await closure()
|
||||||
|
}
|
||||||
|
}, for: controlEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
26
AutoCat2/Components/Extensions/UIStackView.swift
Normal file
26
AutoCat2/Components/Extensions/UIStackView.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// UIStackView.swift
|
||||||
|
// AutoCat2
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 20.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIStackView {
|
||||||
|
|
||||||
|
func removeAllArrangedSubviews() {
|
||||||
|
|
||||||
|
let removedSubviews = arrangedSubviews.reduce([]) { (allSubviews, subview) -> [UIView] in
|
||||||
|
self.removeArrangedSubview(subview)
|
||||||
|
return allSubviews + [subview]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate all constraints
|
||||||
|
NSLayoutConstraint.deactivate(removedSubviews.flatMap({ $0.constraints }))
|
||||||
|
|
||||||
|
// Remove the views from self
|
||||||
|
removedSubviews.forEach({ $0.removeFromSuperview() })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
18
AutoCat2/Components/Extensions/UITextField.swift
Normal file
18
AutoCat2/Components/Extensions/UITextField.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 06.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension UITextField {
|
||||||
|
|
||||||
|
var textPublisher: AnyPublisher<String?, Never> {
|
||||||
|
|
||||||
|
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: self)
|
||||||
|
.map { ($0.object as? UITextField)?.text }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
25
AutoCat2/Components/Extensions/UIView.swift
Normal file
25
AutoCat2/Components/Extensions/UIView.swift
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// UIView.swift
|
||||||
|
// AutoCat2
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 20.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIView {
|
||||||
|
|
||||||
|
func disableTranslatesAutoresizingMaskIntoConstraints() -> Self {
|
||||||
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func pin(to view: UIView, insets: UIEdgeInsets = .zero) {
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: insets.left),
|
||||||
|
trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -insets.right),
|
||||||
|
topAnchor.constraint(equalTo: view.topAnchor, constant: insets.top),
|
||||||
|
bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -insets.bottom)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
16
AutoCat2/Components/Extensions/UIViewController.swift
Normal file
16
AutoCat2/Components/Extensions/UIViewController.swift
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 10.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIViewController {
|
||||||
|
|
||||||
|
func show(error: Error) {
|
||||||
|
|
||||||
|
let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
216
AutoCat2/Components/PNKeyboard.swift
Normal file
216
AutoCat2/Components/PNKeyboard.swift
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import UIKit
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
protocol PNKeyboardDelegate: AnyObject {
|
||||||
|
func returnClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol PNButtonDelegate: AnyObject {
|
||||||
|
func buttonTapped(_ button: PNButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PNButtonType {
|
||||||
|
case symbol(String)
|
||||||
|
case backspace
|
||||||
|
case done
|
||||||
|
}
|
||||||
|
|
||||||
|
class PNButton: UIButton {
|
||||||
|
private(set) var type: PNButtonType
|
||||||
|
private var timer: Timer?
|
||||||
|
private var waitCount = 0
|
||||||
|
private var rectLayer = CAShapeLayer()
|
||||||
|
private var bgColor = UIColor(named: "KeyBackground")
|
||||||
|
|
||||||
|
weak var delegate: PNButtonDelegate?
|
||||||
|
|
||||||
|
init(letter: Character) {
|
||||||
|
self.type = .symbol(String(letter))
|
||||||
|
super.init(frame: .zero)
|
||||||
|
self.setup()
|
||||||
|
self.titleLabel?.font = UIFont(name: "RoadNumbers", size: 36)
|
||||||
|
let title = String(Constants.pnLettersMap[letter] ?? letter)
|
||||||
|
self.setTitle(title, for: .normal)
|
||||||
|
self.setTitleColor(.label, for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(digit: Int) {
|
||||||
|
self.type = .symbol(String(digit))
|
||||||
|
super.init(frame: .zero)
|
||||||
|
self.setup()
|
||||||
|
self.titleLabel?.font = UIFont(name: "RoadNumbersCyr-Regular", size: 30)
|
||||||
|
let character = Character(String(digit))
|
||||||
|
let title = String(Constants.pnLettersMap[character] ?? character)
|
||||||
|
self.setTitle(title, for: .normal)
|
||||||
|
self.setTitleColor(.label, for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(imageName: String, type: PNButtonType) {
|
||||||
|
self.type = type
|
||||||
|
self.bgColor = UIColor(named: "DarkKeyBackground")
|
||||||
|
super.init(frame: .zero)
|
||||||
|
self.setup()
|
||||||
|
self.setImage(UIImage(systemName: imageName), for: .normal)
|
||||||
|
self.tintColor = .label
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup(_ radius: CGFloat = 0.5) {
|
||||||
|
self.rectLayer.cornerRadius = 6
|
||||||
|
self.layer.addSublayer(self.rectLayer)
|
||||||
|
self.imageView?.layer.zPosition = 2
|
||||||
|
|
||||||
|
self.rectLayer.shadowColor = UIColor.black.cgColor
|
||||||
|
self.rectLayer.shadowOpacity = 0.4
|
||||||
|
self.rectLayer.shadowOffset = CGSize(width: 0.0, height : 1.0)
|
||||||
|
self.rectLayer.shadowRadius = radius
|
||||||
|
|
||||||
|
self.addTarget(self, action: #selector(buttonDown), for: .touchDown)
|
||||||
|
self.addTarget(self, action: #selector(buttonUp), for: [.touchUpInside, .touchUpOutside])
|
||||||
|
self.addTarget(self, action: #selector(touchUpInside), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
self.rectLayer.backgroundColor = self.bgColor?.cgColor
|
||||||
|
self.rectLayer.frame = self.layer.bounds.inset(by: UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func buttonDown(_ sender: PNButton) {
|
||||||
|
sender.layer.opacity = 0.3
|
||||||
|
self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { _ in
|
||||||
|
self.waitCount += 1
|
||||||
|
if self.waitCount > 5 {
|
||||||
|
self.delegate?.buttonTapped(self)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func buttonUp(_ sender: PNButton) {
|
||||||
|
sender.layer.opacity = 1
|
||||||
|
self.timer?.invalidate()
|
||||||
|
self.timer = nil
|
||||||
|
self.waitCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func touchUpInside(_ sender: PNButton) {
|
||||||
|
self.delegate?.buttonTapped(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBacgroundColor(color: UIColor) {
|
||||||
|
self.bgColor = color
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PNKeyboard: UIView, UIInputViewAudioFeedback, PNButtonDelegate {
|
||||||
|
private weak var target: UIKeyInput?
|
||||||
|
weak var delegate: PNKeyboardDelegate?
|
||||||
|
|
||||||
|
private let insets: UIEdgeInsets
|
||||||
|
|
||||||
|
init(target: UIKeyInput?, insets: UIEdgeInsets) {
|
||||||
|
self.target = target
|
||||||
|
self.insets = insets
|
||||||
|
super.init(frame: .zero)
|
||||||
|
self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
|
||||||
|
let blurEffectView = UIVisualEffectView(effect: UIBlurEffect())
|
||||||
|
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
self.addSubview(blurEffectView)
|
||||||
|
|
||||||
|
self.setupButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupButtons() {
|
||||||
|
let letters: [PNButton] = Constants.pnLettersMap.keys.sorted().map { letter in
|
||||||
|
let button = PNButton(letter: letter)
|
||||||
|
button.delegate = self
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
let digits: [PNButton] = [1,2,3,4,5,6,7,8,9,0].map { digit in
|
||||||
|
let button = PNButton(digit: digit)
|
||||||
|
button.delegate = self
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
let backspace = PNButton(imageName: "delete.left", type: .backspace)
|
||||||
|
backspace.delegate = self
|
||||||
|
|
||||||
|
let done = PNButton(imageName: "arrow.turn.down.left", type: .done)
|
||||||
|
done.delegate = self
|
||||||
|
done.setBacgroundColor(color: .systemBlue)
|
||||||
|
done.tintColor = .white
|
||||||
|
|
||||||
|
let letterRows = [
|
||||||
|
self.createLetterStack([letters[0], letters[1], letters[2]]),
|
||||||
|
self.createLetterStack([letters[3], letters[4], letters[5]]),
|
||||||
|
self.createLetterStack([letters[6], letters[7], letters[8]]),
|
||||||
|
self.createLetterStack([letters[9], letters[10], letters[11]])
|
||||||
|
]
|
||||||
|
|
||||||
|
let lettersStack = UIStackView(arrangedSubviews: letterRows)
|
||||||
|
lettersStack.axis = .vertical
|
||||||
|
lettersStack.distribution = .fillEqually
|
||||||
|
//lettersStack.spacing = 8
|
||||||
|
|
||||||
|
let digitRows = [
|
||||||
|
self.createLetterStack([digits[0], digits[1], digits[2]]),
|
||||||
|
self.createLetterStack([digits[3], digits[4], digits[5]]),
|
||||||
|
self.createLetterStack([digits[6], digits[7], digits[8]]),
|
||||||
|
self.createLetterStack([digits[9], backspace, done])
|
||||||
|
]
|
||||||
|
|
||||||
|
let digitsStack = UIStackView(arrangedSubviews: digitRows)
|
||||||
|
digitsStack.axis = .vertical
|
||||||
|
digitsStack.distribution = .fillEqually
|
||||||
|
//digitsStack.spacing = 8
|
||||||
|
|
||||||
|
let mainStack = UIStackView(arrangedSubviews: [lettersStack, digitsStack])
|
||||||
|
mainStack.spacing = 16
|
||||||
|
mainStack.distribution = .fillEqually
|
||||||
|
mainStack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
self.addSubview(mainStack)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
mainStack.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: insets.left),
|
||||||
|
mainStack.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -insets.right),
|
||||||
|
mainStack.topAnchor.constraint(equalTo: self.topAnchor, constant: insets.top),
|
||||||
|
mainStack.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: -insets.bottom)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func createLetterStack(_ views: [UIView]) -> UIStackView {
|
||||||
|
let stack = UIStackView(arrangedSubviews: views)
|
||||||
|
stack.distribution = .fillEqually
|
||||||
|
//stack.spacing = 8
|
||||||
|
return stack
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - PNButtonDelegate
|
||||||
|
|
||||||
|
func buttonTapped(_ button: PNButton) {
|
||||||
|
UIDevice.current.playInputClick()
|
||||||
|
|
||||||
|
switch button.type {
|
||||||
|
case .symbol(let s):
|
||||||
|
self.target?.insertText(s)
|
||||||
|
case .backspace:
|
||||||
|
self.target?.deleteBackward()
|
||||||
|
case .done:
|
||||||
|
self.delegate?.returnClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIInputViewAudioFeedback
|
||||||
|
|
||||||
|
var enableInputClicksWhenVisible: Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
31
AutoCat2/Components/PlateView/FlagLayer.swift
Normal file
31
AutoCat2/Components/PlateView/FlagLayer.swift
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FlagLayer: CALayer {
|
||||||
|
let pixelWidth = 1/UIScreen.main.scale
|
||||||
|
|
||||||
|
// Flag colors - https://ru.wikipedia.org/wiki/%D0%A4%D0%BB%D0%B0%D0%B3_%D0%A0%D0%BE%D1%81%D1%81%D0%B8%D0%B8
|
||||||
|
let blue = CGColor(srgbRed: 0, green: 57/256.0, blue: 166/256.0, alpha: 1)
|
||||||
|
let red = CGColor(srgbRed: 213/256.0, green: 43/256.0, blue: 30/256.0, alpha: 1)
|
||||||
|
|
||||||
|
override func draw(in ctx: CGContext) {
|
||||||
|
ctx.saveGState()
|
||||||
|
super.draw(in: ctx)
|
||||||
|
|
||||||
|
ctx.setStrokeColor(UIColor.black.cgColor)
|
||||||
|
ctx.setLineWidth(0)
|
||||||
|
|
||||||
|
ctx.setFillColor(UIColor.white.cgColor)
|
||||||
|
ctx.fill(bounds.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: bounds.height*2/3, right: 0)))
|
||||||
|
|
||||||
|
ctx.setFillColor(self.blue)
|
||||||
|
ctx.fill(bounds.insetBy(dx: 0, dy: bounds.height/3))
|
||||||
|
|
||||||
|
ctx.setFillColor(self.red)
|
||||||
|
ctx.fill(bounds.inset(by: UIEdgeInsets(top: bounds.height*2/3, left: 0, bottom: 0, right: 0)))
|
||||||
|
|
||||||
|
ctx.setLineWidth(pixelWidth)
|
||||||
|
ctx.stroke(bounds.insetBy(dx: pixelWidth/2, dy: pixelWidth/2))
|
||||||
|
|
||||||
|
ctx.restoreGState()
|
||||||
|
}
|
||||||
|
}
|
||||||
138
AutoCat2/Components/PlateView/PlateView.swift
Normal file
138
AutoCat2/Components/PlateView/PlateView.swift
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import UIKit
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
class PlateView: UIView {
|
||||||
|
// Some driver plate parameters from "ГОСТ Р 50577-93"
|
||||||
|
// http://docs.cntd.ru/document/gost-r-50577-93
|
||||||
|
private static let aspectRatio: CGFloat = 112.0/520.0
|
||||||
|
private static let fontHeightCoeff: CGFloat = 58.0/76.0
|
||||||
|
|
||||||
|
private var bgLayer = CALayer()
|
||||||
|
private var mainBgLayer = CALayer()
|
||||||
|
private var regionBgLayer = CALayer()
|
||||||
|
private var numberLayer = CenterTextLayer(coeff: fontHeightCoeff)
|
||||||
|
private var regionLayer = CenterTextLayer(coeff: fontHeightCoeff)
|
||||||
|
private var countryLayer = CenterTextLayer()
|
||||||
|
private var flagLayer = FlagLayer()
|
||||||
|
|
||||||
|
var number: PlateNumber? {
|
||||||
|
didSet {
|
||||||
|
self.layoutSubviews()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var foreground: UIColor? {
|
||||||
|
didSet {
|
||||||
|
self.layoutSubviews()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
self.layer.backgroundColor = UIColor.clear.cgColor
|
||||||
|
|
||||||
|
self.bgLayer.cornerRadius = 6 //self.bounds.height/8
|
||||||
|
self.layer.addSublayer(self.bgLayer)
|
||||||
|
|
||||||
|
self.mainBgLayer.cornerRadius = 4
|
||||||
|
self.layer.addSublayer(self.mainBgLayer)
|
||||||
|
|
||||||
|
self.regionBgLayer.cornerRadius = 4
|
||||||
|
self.layer.addSublayer(self.regionBgLayer)
|
||||||
|
|
||||||
|
self.numberLayer.alignmentMode = .center
|
||||||
|
self.numberLayer.contentsScale = UIScreen.main.scale
|
||||||
|
self.layer.addSublayer(self.numberLayer)
|
||||||
|
|
||||||
|
self.regionLayer.alignmentMode = .center
|
||||||
|
self.regionLayer.contentsScale = UIScreen.main.scale
|
||||||
|
self.layer.addSublayer(self.regionLayer)
|
||||||
|
|
||||||
|
self.countryLayer.contentsScale = UIScreen.main.scale
|
||||||
|
self.countryLayer.string = "RUS"
|
||||||
|
self.countryLayer.alignmentMode = .center
|
||||||
|
self.layer.addSublayer(self.countryLayer)
|
||||||
|
|
||||||
|
self.flagLayer.contentsScale = UIScreen.main.scale
|
||||||
|
self.layer.addSublayer(self.flagLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pathForFlag(in rect: CGRect) -> CGPath {
|
||||||
|
let path = CGMutablePath()
|
||||||
|
let rect = CGPath(rect: rect, transform: nil)
|
||||||
|
path.addPath(rect)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
guard let number = self.number else { return }
|
||||||
|
guard let fgColorMain = UIColor(named: "PlateForeground")?.cgColor else { return }
|
||||||
|
guard let bgColor = UIColor(named: "PlateBackground")?.cgColor else { return }
|
||||||
|
|
||||||
|
let fgColor = self.foreground?.cgColor ?? fgColorMain
|
||||||
|
|
||||||
|
self.bgLayer.backgroundColor = fgColor
|
||||||
|
self.bgLayer.frame = self.bounds
|
||||||
|
|
||||||
|
self.mainBgLayer.backgroundColor = bgColor
|
||||||
|
self.regionBgLayer.backgroundColor = bgColor
|
||||||
|
self.mainBgLayer.frame = self.bounds.inset(by: UIEdgeInsets(top: 2, left: 2, bottom: 2, right: self.bounds.width*0.27 + 1))
|
||||||
|
self.regionBgLayer.frame = self.bounds.inset(by: UIEdgeInsets(top: 2, left: self.bounds.width*0.73 + 1, bottom: 2, right: 2))
|
||||||
|
|
||||||
|
self.numberLayer.frame = self.mainBgLayer.frame.insetBy(dx: 4, dy: 0)
|
||||||
|
let font = UIFont(name: "RoadNumbers", size: self.mainBgLayer.frame.height*1.1) ?? UIFont.boldSystemFont(ofSize: 24)
|
||||||
|
let attributes: [NSAttributedString.Key: Any] = [
|
||||||
|
.kern: 3,
|
||||||
|
.font: font,
|
||||||
|
.foregroundColor: fgColor
|
||||||
|
]
|
||||||
|
let attributed = NSAttributedString(string: number.mainPart(), attributes: attributes)
|
||||||
|
self.numberLayer.string = attributed
|
||||||
|
|
||||||
|
let rbgSize = self.regionBgLayer.frame.size
|
||||||
|
|
||||||
|
self.regionLayer.frame = self.regionBgLayer.frame.inset(by: UIEdgeInsets(top: 2, left: 2, bottom: rbgSize.height*0.35, right: 2))
|
||||||
|
let regionFont = UIFont(name: "RoadNumbers", size: rbgSize.height*0.8) ?? UIFont.boldSystemFont(ofSize: 24)
|
||||||
|
let regionAttrs: [NSAttributedString.Key: Any] = [
|
||||||
|
.kern: 1,
|
||||||
|
.font: regionFont,
|
||||||
|
.foregroundColor: fgColor
|
||||||
|
]
|
||||||
|
let attributedRegion = NSAttributedString(string: number.region(), attributes: regionAttrs)
|
||||||
|
self.regionLayer.string = attributedRegion
|
||||||
|
|
||||||
|
self.countryLayer.foregroundColor = fgColor
|
||||||
|
self.countryLayer.frame = self.regionBgLayer.frame.inset(by: UIEdgeInsets(top: rbgSize.height*0.64, left: rbgSize.width*0.08, bottom: rbgSize.height*0.07, right: rbgSize.width*0.42))
|
||||||
|
self.countryLayer.fontSize = self.countryLayer.frame.size.height
|
||||||
|
|
||||||
|
let top = (self.regionBgLayer.frame.origin.y + rbgSize.height*0.68).rounded(.toNearestOrAwayFromZero)
|
||||||
|
let left = (self.regionBgLayer.frame.origin.x + rbgSize.width*0.62).rounded(.toNearestOrAwayFromZero)
|
||||||
|
let w = (rbgSize.width*0.34).rounded(.toNearestOrAwayFromZero)
|
||||||
|
let h = (rbgSize.height*0.28).rounded(.toNearestOrAwayFromZero)
|
||||||
|
self.flagLayer.frame = CGRect(x: left, y: top, width: w, height: h)
|
||||||
|
self.flagLayer.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
guard self.bounds.width != 0 || self.bounds.height != 0 else {
|
||||||
|
return CGSize.zero
|
||||||
|
}
|
||||||
|
|
||||||
|
let curAspectRatio = self.bounds.height/self.bounds.width
|
||||||
|
if curAspectRatio >= PlateView.aspectRatio {
|
||||||
|
return CGSize(width: self.bounds.width, height: self.bounds.width*PlateView.aspectRatio)
|
||||||
|
} else {
|
||||||
|
return CGSize(width: self.bounds.height/PlateView.aspectRatio, height: self.bounds.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
AutoCat2/Controllers/AuthController.swift
Normal file
99
AutoCat2/Controllers/AuthController.swift
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 06.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import PKHUD
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
class AuthController: UIViewController {
|
||||||
|
|
||||||
|
private lazy var emailField = ACTextField(placeholder: "Email")
|
||||||
|
.onTextChanged(textChanged)
|
||||||
|
.keyboardType(.emailAddress)
|
||||||
|
|
||||||
|
private lazy var passwordField = ACTextField(placeholder: "Password")
|
||||||
|
.onTextChanged(textChanged)
|
||||||
|
.secure(true)
|
||||||
|
|
||||||
|
private lazy var loginButton = ACButton(title: "Log in", onTapAsync: loginTapped)
|
||||||
|
.enable(false)
|
||||||
|
|
||||||
|
private lazy var signupButton = ACButton(title: "Sign up", onTapAsync: signupTapped)
|
||||||
|
.enable(false)
|
||||||
|
|
||||||
|
private lazy var stackView: UIStackView = {
|
||||||
|
let stack = UIStackView(arrangedSubviews: [
|
||||||
|
emailField,
|
||||||
|
passwordField,
|
||||||
|
loginButton,
|
||||||
|
signupButton
|
||||||
|
])
|
||||||
|
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.spacing = 16
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = .systemBackground
|
||||||
|
view.addSubview(stackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||||
|
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||||
|
stackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.7),
|
||||||
|
loginButton.heightAnchor.constraint(equalToConstant: 40),
|
||||||
|
signupButton.heightAnchor.constraint(equalToConstant: 40)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func textChanged(_ text: String?) {
|
||||||
|
guard let email = emailField.text, let pass = passwordField.text else {
|
||||||
|
loginButton.isEnabled = false
|
||||||
|
signupButton.isEnabled = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let enabled = email.count >= 5 && pass.count >= 5
|
||||||
|
loginButton.isEnabled = enabled
|
||||||
|
signupButton.isEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Button handlers
|
||||||
|
|
||||||
|
func loginTapped() async {
|
||||||
|
guard let email = emailField.text, let password = passwordField.text else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
HUD.show(.progress)
|
||||||
|
Settings.shared.user = try await Api.shared.login(email: email, password: password)
|
||||||
|
view.window?.rootViewController = MainTabController()
|
||||||
|
HUD.hide()
|
||||||
|
} catch {
|
||||||
|
HUD.hide()
|
||||||
|
show(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func signupTapped() async {
|
||||||
|
guard let email = emailField.text, let password = passwordField.text else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
HUD.show(.progress)
|
||||||
|
Settings.shared.user = try await Api.shared.signup(email: email, password: password)
|
||||||
|
view.window?.rootViewController = MainTabController()
|
||||||
|
HUD.hide()
|
||||||
|
} catch {
|
||||||
|
HUD.hide()
|
||||||
|
show(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
AutoCat2/Controllers/CheckController.swift
Normal file
112
AutoCat2/Controllers/CheckController.swift
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
//
|
||||||
|
// CheckController.swift
|
||||||
|
// AutoCat2
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 20.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class CheckController: UIViewController {
|
||||||
|
|
||||||
|
public var onCheck: ((String) -> Void)?
|
||||||
|
|
||||||
|
private lazy var keyboardView: PNKeyboard = {
|
||||||
|
let keyboard = PNKeyboard(target: self.numberField, insets: .zero)
|
||||||
|
keyboard.delegate = self
|
||||||
|
return keyboard
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var numberField: SwiftMaskTextfield = {
|
||||||
|
let textField = SwiftMaskTextfield()
|
||||||
|
textField.formatPattern = "@###@@###"
|
||||||
|
textField.placeholder = "A001AA 777"
|
||||||
|
textField.borderStyle = .roundedRect
|
||||||
|
textField.font = .preferredFont(forTextStyle: .largeTitle)
|
||||||
|
textField.textAlignment = .center
|
||||||
|
textField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
|
||||||
|
return textField
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let plateView = PlateView(frame: .zero)
|
||||||
|
|
||||||
|
private lazy var checkButton = ACButton(title: "Check", onTap: check)
|
||||||
|
.enable(false)
|
||||||
|
|
||||||
|
private lazy var stackView: UIStackView = {
|
||||||
|
let stack = UIStackView(arrangedSubviews: [plateView, checkButton])
|
||||||
|
stack.axis = .horizontal
|
||||||
|
stack.spacing = 16
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let titleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "Check new plate number"
|
||||||
|
label.font = UIFont.preferredFont(forTextStyle: .headline)
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var mainStackView: UIStackView = {
|
||||||
|
let stack = UIStackView(arrangedSubviews: [titleLabel, stackView, keyboardView])
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.spacing = 16
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
//view.backgroundColor = .systemBackground
|
||||||
|
checkButton.contentEdgeInsets = .init(top: 0, left: 8, bottom: 0, right: 8)
|
||||||
|
|
||||||
|
view.addSubview(mainStackView)
|
||||||
|
mainStackView.pin(to: view, insets: .init(top: 16, left: 16, bottom: 16, right: 16))
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
plateView.heightAnchor.constraint(equalToConstant: 40)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
|
self.numberField.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func check() {
|
||||||
|
guard let number = numberField.text else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let numberNormalized = number.filter { !$0.isWhitespace }.uppercased()
|
||||||
|
numberField.resignFirstResponder()
|
||||||
|
onCheck?(numberNormalized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CheckController: PNKeyboardDelegate {
|
||||||
|
|
||||||
|
func returnClicked() {
|
||||||
|
check()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CheckController: UITextFieldDelegate {
|
||||||
|
|
||||||
|
@objc func textFieldDidChange(_ textField: UITextField) {
|
||||||
|
guard let text = textField.text else {
|
||||||
|
self.checkButton.isEnabled = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.checkButton.isEnabled = text.count >= 8
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
37
AutoCat2/Controllers/HistoryController.swift
Normal file
37
AutoCat2/Controllers/HistoryController.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 13.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import PKHUD
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
class HistoryController: UIViewController {
|
||||||
|
|
||||||
|
|
||||||
|
private lazy var tableView: UITableView = {
|
||||||
|
let table = UITableView()
|
||||||
|
table.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return table
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
title = "Check history"
|
||||||
|
|
||||||
|
view.addSubview(tableView)
|
||||||
|
tableView.pin(to: view)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPlateNumber(_ number: String) async {
|
||||||
|
print("Check plate number: ", number)
|
||||||
|
|
||||||
|
do {
|
||||||
|
HUD.show(.progress)
|
||||||
|
try await VehicleService.shared.check(plateNumber: number, force: false)
|
||||||
|
} catch {
|
||||||
|
show(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
AutoCat2/Controllers/MainTabController.swift
Normal file
62
AutoCat2/Controllers/MainTabController.swift
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 06.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftEntryKit
|
||||||
|
|
||||||
|
class MainTabController: UITabBarController, UITabBarControllerDelegate {
|
||||||
|
|
||||||
|
// Dummy controller, tabbar item will be used as a button
|
||||||
|
private let addController = UIViewController()
|
||||||
|
|
||||||
|
let historyController = HistoryController()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.view.backgroundColor = .systemBackground
|
||||||
|
self.delegate = self
|
||||||
|
|
||||||
|
let historyNavController = UINavigationController(rootViewController: historyController)
|
||||||
|
historyNavController.tabBarItem = UITabBarItem(title: "Check", image: UIImage(systemName: "eye"), selectedImage: UIImage(systemName: "eye.fill"))
|
||||||
|
|
||||||
|
let settingsController = SettingsController()
|
||||||
|
settingsController.tabBarItem = UITabBarItem(title: "Settings", image: UIImage(systemName: "gear"), selectedImage: nil)
|
||||||
|
|
||||||
|
addController.tabBarItem = UITabBarItem(title: "New", image: UIImage(systemName: "plus"), selectedImage: nil)
|
||||||
|
|
||||||
|
self.viewControllers = [historyNavController, addController, settingsController]
|
||||||
|
}
|
||||||
|
|
||||||
|
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
|
||||||
|
if viewController == addController {
|
||||||
|
showCheckPuller()
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showCheckPuller() {
|
||||||
|
var attributes = EKAttributes.bottomToast
|
||||||
|
attributes.displayDuration = .infinity
|
||||||
|
attributes.positionConstraints.keyboardRelation = .bind(offset: .none)
|
||||||
|
attributes.entryBackground = .color(color: .standardBackground)
|
||||||
|
attributes.screenBackground = .color(color: EKColor(UIColor(white: 0.3, alpha: 0.5)))
|
||||||
|
attributes.roundCorners = .top(radius: 24)
|
||||||
|
attributes.screenInteraction = .dismiss
|
||||||
|
attributes.entryInteraction = .forward
|
||||||
|
//attributes.shadow = .active(with: .init(color: .black, opacity: 0.2, radius: 24, offset: .zero))
|
||||||
|
|
||||||
|
let checkController = CheckController()
|
||||||
|
checkController.onCheck = { number in
|
||||||
|
SwiftEntryKit.dismiss()
|
||||||
|
Task {
|
||||||
|
await self.historyController.checkPlateNumber(number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SwiftEntryKit.display(entry: checkController, using: attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
AutoCat2/Controllers/SettingsController.swift
Normal file
12
AutoCat2/Controllers/SettingsController.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 13.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SettingsController: UIViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
}
|
||||||
|
}
|
||||||
30
AutoCat2/Info.plist
Normal file
30
AutoCat2/Info.plist
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>UIAppFonts</key>
|
||||||
|
<array>
|
||||||
|
<string>RoadNumbers.otf</string>
|
||||||
|
<string>RoadNumbers2.0.otf</string>
|
||||||
|
</array>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>Default Configuration</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
63
AutoCat2/SceneDelegate.swift
Normal file
63
AutoCat2/SceneDelegate.swift
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
//
|
||||||
|
// SceneDelegate.swift
|
||||||
|
// AutoCat2
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 05.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
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 = (scene as? UIWindowScene) else { return }
|
||||||
|
|
||||||
|
self.window = UIWindow(windowScene: scene)
|
||||||
|
|
||||||
|
if Settings.shared.user.token.isEmpty {
|
||||||
|
self.window?.rootViewController = AuthController()
|
||||||
|
} else {
|
||||||
|
self.window?.rootViewController = MainTabController()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.window?.makeKeyAndVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDidDisconnect(_ scene: UIScene) {
|
||||||
|
// Called as the scene is being released by the system.
|
||||||
|
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
||||||
|
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
||||||
|
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||||
|
// Called when the scene has moved from an inactive state to an active state.
|
||||||
|
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneWillResignActive(_ scene: UIScene) {
|
||||||
|
// Called when the scene will move from an active state to an inactive state.
|
||||||
|
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||||
|
// Called as the scene transitions from the background to the foreground.
|
||||||
|
// Use this method to undo the changes made on entering the background.
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||||
|
// Called as the scene transitions from the foreground to the background.
|
||||||
|
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||||
|
// to restore the scene back to its current state.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
51
AutoCat2/ThirdParty/CenterTextLayer.swift
vendored
Normal file
51
AutoCat2/ThirdParty/CenterTextLayer.swift
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
//
|
||||||
|
// CenterTextLayer.swift
|
||||||
|
// CenterTextLayer
|
||||||
|
//
|
||||||
|
// Created by Cem Olcay on 12/04/2017.
|
||||||
|
//
|
||||||
|
// http://stackoverflow.com/a/41518502/2048130
|
||||||
|
//
|
||||||
|
|
||||||
|
#if os(iOS) || os(tvOS)
|
||||||
|
import UIKit
|
||||||
|
#elseif os(OSX)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public class CenterTextLayer: CATextLayer {
|
||||||
|
|
||||||
|
var heightCoefficient: CGFloat = 1
|
||||||
|
|
||||||
|
public override init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override init(layer: Any) {
|
||||||
|
super.init(layer: layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
public required init(coder aDecoder: NSCoder) {
|
||||||
|
super.init(layer: aDecoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(coeff: CGFloat) {
|
||||||
|
super.init()
|
||||||
|
self.heightCoefficient = coeff
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func draw(in ctx: CGContext) {
|
||||||
|
#if os(iOS) || os(tvOS)
|
||||||
|
let multiplier = CGFloat(1)
|
||||||
|
#elseif os(OSX)
|
||||||
|
let multiplier = CGFloat(-1)
|
||||||
|
#endif
|
||||||
|
let h = ((string as? NSAttributedString)?.size().height ?? fontSize)*heightCoefficient;
|
||||||
|
let yDiff = (bounds.size.height - h) / 2 * multiplier
|
||||||
|
|
||||||
|
ctx.saveGState()
|
||||||
|
ctx.translateBy(x: 0.0, y: yDiff)
|
||||||
|
super.draw(in: ctx)
|
||||||
|
ctx.restoreGState()
|
||||||
|
}
|
||||||
|
}
|
||||||
269
AutoCat2/ThirdParty/SwiftMaskTextfield.swift
vendored
Normal file
269
AutoCat2/ThirdParty/SwiftMaskTextfield.swift
vendored
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2016 Gabriel Maccori Kozma
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
//**********************************************************************************************************
|
||||||
|
//
|
||||||
|
// MARK: - Constants -
|
||||||
|
//
|
||||||
|
//**********************************************************************************************************
|
||||||
|
|
||||||
|
//**********************************************************************************************************
|
||||||
|
//
|
||||||
|
// MARK: - Definitions -
|
||||||
|
//
|
||||||
|
//**********************************************************************************************************
|
||||||
|
|
||||||
|
//**********************************************************************************************************
|
||||||
|
//
|
||||||
|
// MARK: - Class -
|
||||||
|
//
|
||||||
|
//**********************************************************************************************************
|
||||||
|
|
||||||
|
open class SwiftMaskTextfield : UITextField {
|
||||||
|
|
||||||
|
//**************************************************
|
||||||
|
// MARK: - Properties
|
||||||
|
//**************************************************
|
||||||
|
|
||||||
|
public let lettersAndDigitsReplacementChar: String = "*"
|
||||||
|
public let anyLetterReplecementChar: String = "@"
|
||||||
|
public let lowerCaseLetterReplecementChar: String = "a"
|
||||||
|
public let upperCaseLetterReplecementChar: String = "A"
|
||||||
|
public let digitsReplecementChar: String = "#"
|
||||||
|
|
||||||
|
/**
|
||||||
|
Var that holds the format pattern that you wish to apply
|
||||||
|
to some text
|
||||||
|
|
||||||
|
If the pattern is set to "" no mask would be applied and
|
||||||
|
the textfield would behave like a normal one
|
||||||
|
*/
|
||||||
|
@IBInspectable open var formatPattern: String = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
Var that holds the prefix to be added to the textfield
|
||||||
|
|
||||||
|
If the prefix is set to "" no string will be added to the beggining
|
||||||
|
of the text
|
||||||
|
*/
|
||||||
|
@IBInspectable open var prefix: String = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
Var that have the maximum length, based on the mask set
|
||||||
|
*/
|
||||||
|
open var maxLength: Int {
|
||||||
|
get {
|
||||||
|
return formatPattern.count + prefix.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Overriding the var text from UITextField so if any text
|
||||||
|
is applied programmatically by calling formatText
|
||||||
|
*/
|
||||||
|
override open var text: String? {
|
||||||
|
set {
|
||||||
|
super.text = newValue
|
||||||
|
self.formatText()
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
return super.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//**************************************************
|
||||||
|
// MARK: - Constructors
|
||||||
|
//**************************************************
|
||||||
|
|
||||||
|
public required init?(coder aDecoder: NSCoder) {
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
self.setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
self.setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.deRegisterForNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
|
//**************************************************
|
||||||
|
// MARK: - Private Methods
|
||||||
|
//**************************************************
|
||||||
|
|
||||||
|
fileprivate func setup() {
|
||||||
|
self.registerForNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func registerForNotifications() {
|
||||||
|
NotificationCenter.default.addObserver(self,
|
||||||
|
selector: #selector(textDidChange),
|
||||||
|
name: NSNotification.Name(rawValue: "UITextFieldTextDidChangeNotification"),
|
||||||
|
object: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func deRegisterForNotifications() {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc fileprivate func textDidChange() {
|
||||||
|
self.undoManager?.removeAllActions()
|
||||||
|
self.formatText()
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func getOnlyDigitsString(_ string: String) -> String {
|
||||||
|
let charactersArray = string.components(separatedBy: CharacterSet.decimalDigits.inverted)
|
||||||
|
return charactersArray.joined(separator: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func getOnlyLettersString(_ string: String) -> String {
|
||||||
|
let charactersArray = string.components(separatedBy: CharacterSet.letters.inverted)
|
||||||
|
return charactersArray.joined(separator: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func getUppercaseLettersString(_ string: String) -> String {
|
||||||
|
let charactersArray = string.components(separatedBy: CharacterSet.uppercaseLetters.inverted)
|
||||||
|
return charactersArray.joined(separator: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func getLowercaseLettersString(_ string: String) -> String {
|
||||||
|
let charactersArray = string.components(separatedBy: CharacterSet.lowercaseLetters.inverted)
|
||||||
|
return charactersArray.joined(separator: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func getFilteredString(_ string: String) -> String {
|
||||||
|
let charactersArray = string.components(separatedBy: CharacterSet.alphanumerics.inverted)
|
||||||
|
return charactersArray.joined(separator: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func getStringWithoutPrefix(_ string: String) -> String {
|
||||||
|
if string.range(of: self.prefix) != nil {
|
||||||
|
if string.count > self.prefix.count {
|
||||||
|
let prefixIndex = string.index(string.endIndex, offsetBy: (string.count - self.prefix.count) * -1)
|
||||||
|
return String(string[prefixIndex...])
|
||||||
|
} else if string.count == self.prefix.count {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
//**************************************************
|
||||||
|
// MARK: - Self Public Methods
|
||||||
|
//**************************************************
|
||||||
|
|
||||||
|
/**
|
||||||
|
Func that formats the text based on formatPattern
|
||||||
|
|
||||||
|
Override this function if you want to customize the behaviour of
|
||||||
|
the class
|
||||||
|
*/
|
||||||
|
open func formatText() {
|
||||||
|
var currentTextForFormatting = ""
|
||||||
|
|
||||||
|
if let text = super.text {
|
||||||
|
if text.count > 0 {
|
||||||
|
currentTextForFormatting = self.getStringWithoutPrefix(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.maxLength > 0 {
|
||||||
|
var formatterIndex = self.formatPattern.startIndex, currentTextForFormattingIndex = currentTextForFormatting.startIndex
|
||||||
|
var finalText = ""
|
||||||
|
|
||||||
|
currentTextForFormatting = self.getFilteredString(currentTextForFormatting)
|
||||||
|
|
||||||
|
if currentTextForFormatting.count > 0 {
|
||||||
|
while true {
|
||||||
|
let formatPatternRange = formatterIndex ..< formatPattern.index(after: formatterIndex)
|
||||||
|
let currentFormatCharacter = String(self.formatPattern[formatPatternRange])
|
||||||
|
|
||||||
|
let currentTextForFormattingPatterRange = currentTextForFormattingIndex ..< currentTextForFormatting.index(after: currentTextForFormattingIndex)
|
||||||
|
let currentTextForFormattingCharacter = String(currentTextForFormatting[currentTextForFormattingPatterRange])
|
||||||
|
|
||||||
|
switch currentFormatCharacter {
|
||||||
|
case self.lettersAndDigitsReplacementChar:
|
||||||
|
finalText += currentTextForFormattingCharacter
|
||||||
|
currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex)
|
||||||
|
formatterIndex = formatPattern.index(after: formatterIndex)
|
||||||
|
case self.anyLetterReplecementChar:
|
||||||
|
let filteredChar = self.getOnlyLettersString(currentTextForFormattingCharacter)
|
||||||
|
if !filteredChar.isEmpty {
|
||||||
|
finalText += filteredChar
|
||||||
|
formatterIndex = formatPattern.index(after: formatterIndex)
|
||||||
|
}
|
||||||
|
currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex)
|
||||||
|
case self.lowerCaseLetterReplecementChar:
|
||||||
|
let filteredChar = self.getLowercaseLettersString(currentTextForFormattingCharacter)
|
||||||
|
if !filteredChar.isEmpty {
|
||||||
|
finalText += filteredChar
|
||||||
|
formatterIndex = formatPattern.index(after: formatterIndex)
|
||||||
|
}
|
||||||
|
currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex)
|
||||||
|
case self.upperCaseLetterReplecementChar:
|
||||||
|
let filteredChar = self.getUppercaseLettersString(currentTextForFormattingCharacter)
|
||||||
|
if !filteredChar.isEmpty {
|
||||||
|
finalText += filteredChar
|
||||||
|
formatterIndex = formatPattern.index(after: formatterIndex)
|
||||||
|
}
|
||||||
|
currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex)
|
||||||
|
case self.digitsReplecementChar:
|
||||||
|
let filteredChar = self.getOnlyDigitsString(currentTextForFormattingCharacter)
|
||||||
|
if !filteredChar.isEmpty {
|
||||||
|
finalText += filteredChar
|
||||||
|
formatterIndex = formatPattern.index(after: formatterIndex)
|
||||||
|
}
|
||||||
|
currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex)
|
||||||
|
default:
|
||||||
|
finalText += currentFormatCharacter
|
||||||
|
formatterIndex = formatPattern.index(after: formatterIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if formatterIndex >= self.formatPattern.endIndex ||
|
||||||
|
currentTextForFormattingIndex >= currentTextForFormatting.endIndex {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if finalText.count > 0 {
|
||||||
|
super.text = "\(self.prefix)\(finalText)"
|
||||||
|
} else {
|
||||||
|
super.text = finalText
|
||||||
|
}
|
||||||
|
|
||||||
|
if let text = self.text {
|
||||||
|
if text.count > self.maxLength {
|
||||||
|
super.text = String(text[text.index(text.startIndex, offsetBy: self.maxLength)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
AutoCat2/ViewController.swift
Normal file
19
AutoCat2/ViewController.swift
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// ViewController.swift
|
||||||
|
// AutoCat2
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 05.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ViewController: UIViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
// Do any additional setup after loading the view.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
protocol ApiMethodMockProtocol {
|
|
||||||
var path: String { get }
|
|
||||||
var httpMethod: String { get }
|
|
||||||
func response(headers: [String: String], params: [String: Any]) -> (status: Int, data: Data?)
|
|
||||||
}
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
extension URL {
|
|
||||||
public var queryParameters: [String: String]? {
|
|
||||||
guard
|
|
||||||
let components = URLComponents(url: self, resolvingAgainstBaseURL: true),
|
|
||||||
let queryItems = components.queryItems else { return nil }
|
|
||||||
return queryItems.reduce(into: [String: String]()) { (result, item) in
|
|
||||||
result[item.name] = item.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension URLRequest {
|
|
||||||
func bodySteamAsJSON() -> [String: Any]? {
|
|
||||||
guard let bodyStream = self.httpBodyStream else { return nil }
|
|
||||||
|
|
||||||
bodyStream.open()
|
|
||||||
|
|
||||||
// Will read 16 chars per iteration. Can use bigger buffer if needed
|
|
||||||
let bufferSize: Int = 16
|
|
||||||
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
|
||||||
var dat = Data()
|
|
||||||
|
|
||||||
while bodyStream.hasBytesAvailable {
|
|
||||||
let readDat = bodyStream.read(buffer, maxLength: bufferSize)
|
|
||||||
dat.append(buffer, count: readDat)
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.deallocate()
|
|
||||||
bodyStream.close()
|
|
||||||
|
|
||||||
return try? JSONSerialization.jsonObject(with: dat, options: JSONSerialization.ReadingOptions.allowFragments) as? [String: Any]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MockURLProtocol: URLProtocol {
|
|
||||||
|
|
||||||
static var baseUrl: String = ""
|
|
||||||
static var apiMethodMocks: [ApiMethodMockProtocol] = []
|
|
||||||
|
|
||||||
override class func canInit(with request: URLRequest) -> Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
|
||||||
return request
|
|
||||||
}
|
|
||||||
|
|
||||||
override func startLoading() {
|
|
||||||
guard let requestUrl = request.url else { return }
|
|
||||||
|
|
||||||
let methodMock = MockURLProtocol.apiMethodMocks.first {
|
|
||||||
return request.url?.absoluteString == MockURLProtocol.baseUrl + $0.path
|
|
||||||
&& request.httpMethod == $0.httpMethod
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let methodMock = methodMock else {
|
|
||||||
if let response = HTTPURLResponse(url: requestUrl, statusCode: 404, httpVersion: "HTTP/2", headerFields: [:]) {
|
|
||||||
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
|
||||||
client?.urlProtocolDidFinishLoading(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assuming we use url parameters in GET requests and JSON-encoded body in everything else
|
|
||||||
var params: [String: Any] = [:]
|
|
||||||
if request.httpBodyStream != nil {
|
|
||||||
if let bodyDict = request.bodySteamAsJSON() {
|
|
||||||
params = bodyDict
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if let urlParams = requestUrl.queryParameters {
|
|
||||||
params = urlParams
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = methodMock.response(headers: request.allHTTPHeaderFields ?? [:], params: params)
|
|
||||||
guard let response = HTTPURLResponse(url: requestUrl, statusCode: result.status, httpVersion: "HTTP/2", headerFields: [:]) else { return }
|
|
||||||
|
|
||||||
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
|
||||||
|
|
||||||
if let data = result.data {
|
|
||||||
client?.urlProtocol(self, didLoad: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
client?.urlProtocolDidFinishLoading(self)
|
|
||||||
|
|
||||||
//client?.urlProtocol(self, didFailWithError: error)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func stopLoading() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import AutoCat2
|
|
||||||
|
|
||||||
open class ApiMethodMock: ApiMethodMockProtocol {
|
|
||||||
|
|
||||||
private(set) var path: String
|
|
||||||
private(set) var httpMethod: String
|
|
||||||
|
|
||||||
init(httpMethod: String, path: String) {
|
|
||||||
self.httpMethod = httpMethod
|
|
||||||
self.path = path
|
|
||||||
}
|
|
||||||
|
|
||||||
func readData(from path: String) -> Data? {
|
|
||||||
guard let url = Bundle(for: type(of: self)).url(forResource: path, withExtension: "json") else { return nil }
|
|
||||||
return try? Data(contentsOf: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
func error(message: String, code: ApiErrorCode? = nil) -> Data? {
|
|
||||||
var errorData: [String: AnyEncodable] = [
|
|
||||||
"success": false,
|
|
||||||
"error": AnyEncodable(message)
|
|
||||||
]
|
|
||||||
|
|
||||||
if let code = code {
|
|
||||||
errorData["errorCode"] = AnyEncodable(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
return try? JSONEncoder().encode(errorData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func notFoundResponse() -> (status: Int, data: Data?) {
|
|
||||||
return (status: 404, data: self.error(message: "Not found"))
|
|
||||||
}
|
|
||||||
|
|
||||||
open func response(headers: [String : String], params: [String : Any]) -> (status: Int, data: Data?) {
|
|
||||||
return self.notFoundResponse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
class LoginMethodMock: ApiMethodMock {
|
|
||||||
private var login: String
|
|
||||||
private var password: String
|
|
||||||
|
|
||||||
init(httpMethod: String, path: String, login: String, password: String) {
|
|
||||||
self.login = login
|
|
||||||
self.password = password
|
|
||||||
super.init(httpMethod: httpMethod, path: path)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func response(headers: [String : String], params: [String : Any]) -> (status: Int, data: Data?) {
|
|
||||||
guard let login = params["email"] as? String, let password = params["password"] as? String else {
|
|
||||||
return (status: 400, data: self.error(message: "Invalid parameters"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if login != self.login || password != self.password {
|
|
||||||
return (status: 200, data: self.error(message: "Incorrect login or password", code: .invalidLoginOrPassword))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (status: 200, data: readData(from: "login_success"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"_id": "832c1bd4-5caa-4c9d-b24c-4c000cd8a793",
|
|
||||||
"email": "selim@fastmail.fm",
|
|
||||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InNlbGltQGZhc3RtYWlsLmZtIiwiaWF0IjoxNjI2MjgwNDgxLCJleHAiOjE2NTc4MTY0ODF9.eU6wpacgCSnhM4EiyMY2lUptsfSfHz9guuvOsAw4X90"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import XCTest
|
|
||||||
import AutoCat2
|
|
||||||
|
|
||||||
class ApiTests: XCTestCase {
|
|
||||||
|
|
||||||
private var api: ApiProtocol!
|
|
||||||
|
|
||||||
private let testLogin = "test@gmail.com"
|
|
||||||
private let testPassword = "12345"
|
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
|
||||||
MockURLProtocol.baseUrl = Constants.baseUrl
|
|
||||||
MockURLProtocol.apiMethodMocks = [
|
|
||||||
LoginMethodMock(httpMethod: "POST", path: "user/login", login: self.testLogin, password: self.testPassword)
|
|
||||||
]
|
|
||||||
|
|
||||||
let sessionConfig = URLSessionConfiguration.default
|
|
||||||
sessionConfig.protocolClasses = [MockURLProtocol.self]
|
|
||||||
let session = URLSession(configuration: sessionConfig)
|
|
||||||
self.api = Api(session: session)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
|
||||||
MockURLProtocol.baseUrl = ""
|
|
||||||
MockURLProtocol.apiMethodMocks = []
|
|
||||||
}
|
|
||||||
|
|
||||||
func testLoginSuccess() async throws {
|
|
||||||
let user = try await self.api.login(email: self.testLogin, password: self.testPassword)
|
|
||||||
XCTAssertTrue(!user.token.isEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testLoginInvalidParams() async throws {
|
|
||||||
do {
|
|
||||||
_ = try await self.api.login(email: "", password: "")
|
|
||||||
} catch let error as ApiError {
|
|
||||||
XCTAssertTrue(error.code == .invalidLoginOrPassword)
|
|
||||||
return
|
|
||||||
} catch {
|
|
||||||
XCTFail("Wrong exception type")
|
|
||||||
}
|
|
||||||
|
|
||||||
XCTFail("Exception expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
AutoCat2Tests/AutoCat2Tests.swift
Normal file
36
AutoCat2Tests/AutoCat2Tests.swift
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// AutoCat2Tests.swift
|
||||||
|
// AutoCat2Tests
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 05.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import AutoCat2
|
||||||
|
|
||||||
|
class AutoCat2Tests: XCTestCase {
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
func testExample() throws {
|
||||||
|
// This is an example of a functional test case.
|
||||||
|
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||||
|
// Any test you write for XCTest can be annotated as throws and async.
|
||||||
|
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
|
||||||
|
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPerformanceExample() throws {
|
||||||
|
// This is an example of a performance test case.
|
||||||
|
self.measure {
|
||||||
|
// Put the code you want to measure the time of here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import XCTest
|
|
||||||
import AutoCat2
|
|
||||||
|
|
||||||
class SettingsTests: XCTestCase {
|
|
||||||
|
|
||||||
private var settings: SettingsProtocol!
|
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
|
||||||
guard let userDefaults = UserDefaults(suiteName: #file) else {
|
|
||||||
throw CocoaError.error("Failed to create UserDefaults")
|
|
||||||
}
|
|
||||||
userDefaults.removePersistentDomain(forName: #file)
|
|
||||||
self.settings = Settings(defaults: userDefaults)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
|
||||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
|
||||||
}
|
|
||||||
|
|
||||||
func testAlternativeOrder() {
|
|
||||||
XCTAssert(self.settings.recognizeAlternativeOrder == false, "recognizeAlternativeOrder: wrong default value")
|
|
||||||
self.settings.recognizeAlternativeOrder = true
|
|
||||||
XCTAssert(self.settings.recognizeAlternativeOrder == true, "recognizeAlternativeOrder: failed to change value")
|
|
||||||
}
|
|
||||||
|
|
||||||
func testShortenedNumbers() {
|
|
||||||
XCTAssert(self.settings.recognizeShortenedNumbers == false, "recognizeShortenedNumbers: wrong default value")
|
|
||||||
self.settings.recognizeShortenedNumbers = true
|
|
||||||
XCTAssert(self.settings.recognizeShortenedNumbers == true, "recognizeShortenedNumbers: failed to change value")
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDefaultRegion() {
|
|
||||||
XCTAssert(self.settings.defaultRegion == "161", "defaultRegion: wrong default value")
|
|
||||||
self.settings.defaultRegion = "761"
|
|
||||||
XCTAssert(self.settings.defaultRegion == "761", "defaultRegion: failed to change value")
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRecordBeep() {
|
|
||||||
XCTAssert(self.settings.recordBeep == false, "recordBeep: wrong default value")
|
|
||||||
self.settings.recordBeep = true
|
|
||||||
XCTAssert(self.settings.recordBeep == true, "recordBeep: failed to change value")
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDebugInfo() {
|
|
||||||
XCTAssert(self.settings.showDebugInfo == false, "showDebugInfo: wrong default value")
|
|
||||||
self.settings.showDebugInfo = true
|
|
||||||
XCTAssert(self.settings.showDebugInfo == true, "showDebugInfo: failed to change value")
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDefaultUser() {
|
|
||||||
XCTAssert(self.settings.user.email.isEmpty, "Default user email is not empty")
|
|
||||||
XCTAssert(self.settings.user.token.isEmpty, "Default user token is not empty")
|
|
||||||
XCTAssert(self.settings.user.firebaseIdToken == nil, "Default user firebase ID token is not nil")
|
|
||||||
XCTAssert(self.settings.user.firebaseRefreshToken == nil, "Default user firebase refresh token is not nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSaveUser() {
|
|
||||||
self.settings.user.token = "TestToken"
|
|
||||||
XCTAssert(self.settings.user.token == "TestToken", "Failed to save user token to settings")
|
|
||||||
|
|
||||||
self.settings.user.firebaseIdToken = "TestFirebaseToken"
|
|
||||||
XCTAssert(self.settings.user.firebaseIdToken == "TestFirebaseToken", "Failed to save user firebaseIdToken to settings")
|
|
||||||
|
|
||||||
self.settings.user.firebaseRefreshToken = "TestResreshToken"
|
|
||||||
XCTAssert(self.settings.user.firebaseRefreshToken == "TestResreshToken", "Failed to save user firebaseRefreshToken to settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
@ -1,13 +1,13 @@
|
|||||||
//
|
//
|
||||||
// Tests_macOS.swift
|
// AutoCat2UITests.swift
|
||||||
// Tests macOS
|
// AutoCat2UITests
|
||||||
//
|
//
|
||||||
// Created by Selim Mustafaev on 04.07.2021.
|
// Created by Selim Mustafaev on 05.03.2022.
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class Tests_macOS: XCTestCase {
|
class AutoCat2UITests: XCTestCase {
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
override func setUpWithError() throws {
|
||||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
32
AutoCat2UITests/AutoCat2UITestsLaunchTests.swift
Normal file
32
AutoCat2UITests/AutoCat2UITestsLaunchTests.swift
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// AutoCat2UITestsLaunchTests.swift
|
||||||
|
// AutoCat2UITests
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 05.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class AutoCat2UITestsLaunchTests: XCTestCase {
|
||||||
|
|
||||||
|
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLaunch() throws {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Insert steps here to perform after app launch but before taking a screenshot,
|
||||||
|
// such as logging into a test account or navigating somewhere in the app
|
||||||
|
|
||||||
|
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||||
|
attachment.name = "Launch Screen"
|
||||||
|
attachment.lifetime = .keepAlways
|
||||||
|
add(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19461" systemVersion="21A559" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21A559" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="VBrand" representedClassName="CDVBrand" syncable="YES" codeGenerationType="class">
|
<entity name="VBrand" representedClassName=".CDVBrand" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="logo" optional="YES" attributeType="String"/>
|
<attribute name="logo" optional="YES" attributeType="String"/>
|
||||||
<relationship name="name" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="VName" inverseName="brand" inverseEntity="VName"/>
|
<relationship name="name" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="VName" inverseName="brand" inverseEntity="VName"/>
|
||||||
<relationship name="vehicle" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Vehicle" inverseName="brand" inverseEntity="Vehicle"/>
|
<relationship name="vehicle" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Vehicle" inverseName="brand" inverseEntity="Vehicle"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Vehicle" representedClassName="CDVehicle" syncable="YES" codeGenerationType="class">
|
<entity name="Vehicle" representedClassName=".CDVehicle" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="currentNumber" optional="YES" attributeType="String"/>
|
<attribute name="currentNumber" optional="YES" attributeType="String"/>
|
||||||
<attribute name="number" optional="YES" attributeType="String"/>
|
<attribute name="number" optional="YES" attributeType="String"/>
|
||||||
<relationship name="brand" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="VBrand" inverseName="vehicle" inverseEntity="VBrand"/>
|
<relationship name="brand" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="VBrand" inverseName="vehicle" inverseEntity="VBrand"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="VName" representedClassName="CDVName" syncable="YES" codeGenerationType="class">
|
<entity name="VName" representedClassName=".CDVName" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="normalized" optional="YES" attributeType="String"/>
|
<attribute name="normalized" optional="YES" attributeType="String"/>
|
||||||
<attribute name="original" optional="YES" attributeType="String"/>
|
<attribute name="original" optional="YES" attributeType="String"/>
|
||||||
<relationship name="brand" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="VBrand" inverseName="name" inverseEntity="VBrand"/>
|
<relationship name="brand" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="VBrand" inverseName="name" inverseEntity="VBrand"/>
|
||||||
13
AutoCatCore/AutoCatCore.docc/AutoCatCore.md
Executable file
13
AutoCatCore/AutoCatCore.docc/AutoCatCore.md
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
# ``AutoCatCore``
|
||||||
|
|
||||||
|
<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@-->
|
||||||
|
|
||||||
|
## Topics
|
||||||
|
|
||||||
|
### <!--@START_MENU_TOKEN@-->Group<!--@END_MENU_TOKEN@-->
|
||||||
|
|
||||||
|
- <!--@START_MENU_TOKEN@-->``Symbol``<!--@END_MENU_TOKEN@-->
|
||||||
18
AutoCatCore/AutoCatCore.h
Normal file
18
AutoCatCore/AutoCatCore.h
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// AutoCatCore.h
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 05.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
//! Project version number for AutoCatCore.
|
||||||
|
FOUNDATION_EXPORT double AutoCatCoreVersionNumber;
|
||||||
|
|
||||||
|
//! Project version string for AutoCatCore.
|
||||||
|
FOUNDATION_EXPORT const unsigned char AutoCatCoreVersionString[];
|
||||||
|
|
||||||
|
// In this header, you should import all the public headers of your framework using statements like #import <AutoCatCore/PublicHeader.h>
|
||||||
|
|
||||||
|
|
||||||
@ -4,7 +4,7 @@ class Response<T>: Decodable where T: Decodable {
|
|||||||
let success: Bool
|
let success: Bool
|
||||||
let data: T?
|
let data: T?
|
||||||
let error: String?
|
let error: String?
|
||||||
let errorCode: ApiErrorCode?
|
let errorCode: Int?
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case success
|
case success
|
||||||
@ -22,7 +22,7 @@ class Response<T>: Decodable where T: Decodable {
|
|||||||
errorCode = nil
|
errorCode = nil
|
||||||
} else {
|
} else {
|
||||||
error = try container.decode(String.self, forKey: .error)
|
error = try container.decode(String.self, forKey: .error)
|
||||||
errorCode = try container.decodeIfPresent(ApiErrorCode.self, forKey: .errorCode)
|
errorCode = try container.decodeIfPresent(Int.self, forKey: .errorCode)
|
||||||
data = nil
|
data = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11,21 +11,37 @@ class StorageService: StorageServiceProtocol {
|
|||||||
|
|
||||||
private let container: NSPersistentCloudKitContainer
|
private let container: NSPersistentCloudKitContainer
|
||||||
|
|
||||||
static let shared = StorageService()
|
static var shared: StorageService {
|
||||||
|
get async throws {
|
||||||
|
print("!!!!!!!!!!!!!!!!!!!!!!!!! StorageService init")
|
||||||
|
let service = StorageService()
|
||||||
|
try await service.loadPersistentStores()
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(inMemory: Bool = false) {
|
init(inMemory: Bool = false) {
|
||||||
|
|
||||||
container = NSPersistentCloudKitContainer(name: "AutoCat2")
|
let bundle = Bundle(for: Self.self)
|
||||||
|
let url = bundle.url(forResource: "AutoCat2", withExtension: "momd")
|
||||||
|
let mom = NSManagedObjectModel(contentsOf: url!)
|
||||||
|
|
||||||
|
container = NSPersistentCloudKitContainer(name: "AutoCat2", managedObjectModel: mom!)
|
||||||
if inMemory {
|
if inMemory {
|
||||||
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
|
||||||
if let error = error as NSError? {
|
private func loadPersistentStores() async throws {
|
||||||
// TODO: Handle error properly
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||||
fatalError("Unresolved error \(error), \(error.userInfo)")
|
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
||||||
}
|
if let error = error as NSError? {
|
||||||
})
|
continuation.resume(throwing: error)
|
||||||
|
} else {
|
||||||
|
continuation.resume(returning: ())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func save() throws {
|
private func save() throws {
|
||||||
@ -5,14 +5,19 @@ protocol VehicleServiceProtocol {
|
|||||||
func check(plateNumber: String, force: Bool) async throws -> CDVehicle
|
func check(plateNumber: String, force: Bool) async throws -> CDVehicle
|
||||||
}
|
}
|
||||||
|
|
||||||
class VehicleService: VehicleServiceProtocol {
|
public class VehicleService: VehicleServiceProtocol {
|
||||||
|
|
||||||
private let api: ApiProtocol
|
private let api: ApiProtocol
|
||||||
private let storage: StorageServiceProtocol
|
private let storage: StorageServiceProtocol
|
||||||
|
|
||||||
static let shared = VehicleService()
|
public static var shared: VehicleService {
|
||||||
|
get async throws {
|
||||||
|
let storageService = try await StorageService.shared
|
||||||
|
return VehicleService(api: Api.shared, storage: storageService)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(api: ApiProtocol = Api.shared, storage: StorageServiceProtocol = StorageService.shared) {
|
init(api: ApiProtocol = Api.shared, storage: StorageServiceProtocol) {
|
||||||
|
|
||||||
self.api = api
|
self.api = api
|
||||||
self.storage = storage
|
self.storage = storage
|
||||||
@ -20,7 +25,8 @@ class VehicleService: VehicleServiceProtocol {
|
|||||||
|
|
||||||
// MARK: - VehicleServiceProtocol
|
// MARK: - VehicleServiceProtocol
|
||||||
|
|
||||||
func check(plateNumber: String, force: Bool) async throws -> CDVehicle {
|
@discardableResult
|
||||||
|
public func check(plateNumber: String, force: Bool) async throws -> CDVehicle {
|
||||||
|
|
||||||
let vehicle = try await api.check(plateNumber: plateNumber, force: force)
|
let vehicle = try await api.check(plateNumber: plateNumber, force: force)
|
||||||
return try storage.store(vehicle: vehicle)
|
return try storage.store(vehicle: vehicle)
|
||||||
@ -30,10 +30,6 @@ public class Api: ApiProtocol {
|
|||||||
|
|
||||||
// MARK: - Private wrappres
|
// MARK: - Private wrappres
|
||||||
|
|
||||||
private func genError(_ msg: String, suggestion: String, code: Int = 0) -> Error {
|
|
||||||
return NSError(domain: "", code: code, userInfo: [NSLocalizedDescriptionKey: msg, NSLocalizedRecoverySuggestionErrorKey: suggestion])
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createRequest<B,P>(api: String, method: String, body: B? = nil, params: [String:P]? = nil) -> URLRequest? where B: Encodable, P: LosslessStringConvertible {
|
private 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 }
|
guard var urlComponents = URLComponents(string: Constants.baseUrl + api) else { return nil }
|
||||||
|
|
||||||
@ -60,38 +56,54 @@ public class Api: ApiProtocol {
|
|||||||
|
|
||||||
private func makeRequest<T,B,P>(api: String, method: String = "GET", body: B?, params: [String:P]? = nil) async throws -> T where T: Decodable, B: Encodable, P: LosslessStringConvertible {
|
private func makeRequest<T,B,P>(api: String, method: String = "GET", body: B?, params: [String:P]? = nil) async throws -> T where T: Decodable, B: Encodable, P: LosslessStringConvertible {
|
||||||
guard let request = self.createRequest(api: api, method: method, body: body, params: params) else {
|
guard let request = self.createRequest(api: api, method: method, body: body, params: params) else {
|
||||||
throw self.genError("Error creating request", suggestion: "")
|
throw ApiError.message("Error creating request")
|
||||||
}
|
}
|
||||||
|
|
||||||
let (data, response) = try await self.session.data(for: request)
|
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<T, Error>) in
|
||||||
guard let httpResponse = response as? HTTPURLResponse else {
|
|
||||||
throw self.genError("non-HTTP response received", suggestion: "")
|
let task = self.session.dataTask(with: request) { data, response, error in
|
||||||
}
|
|
||||||
|
if let error = error {
|
||||||
// let str = String(data: data, encoding: .utf8)
|
continuation.resume(throwing: error)
|
||||||
// print("================================")
|
return
|
||||||
// if let string = str?.replacingOccurrences(of: "\\\"", with: "\"")
|
}
|
||||||
// .replacingOccurrences(of: "\\'", with: "'")
|
|
||||||
// .replacingOccurrences(of: "\\n", with: "") {
|
guard let data = data, data.count > 0 else {
|
||||||
// print(string)
|
continuation.resume(throwing: ApiError.message("No data in response"))
|
||||||
// }
|
return
|
||||||
// print("================================")
|
}
|
||||||
|
|
||||||
do {
|
|
||||||
if data.count > 0 {
|
// let str = String(data: data, encoding: .utf8)
|
||||||
let resp = try JSONDecoder().decode(Response<T>.self, from: data)
|
// print("================================")
|
||||||
if resp.success {
|
// if let string = str?.replacingOccurrences(of: "\\\"", with: "\"")
|
||||||
return resp.data!
|
// .replacingOccurrences(of: "\\'", with: "'")
|
||||||
} else {
|
// .replacingOccurrences(of: "\\n", with: "") {
|
||||||
throw ApiError(httpStatus: httpResponse.statusCode, message: resp.error, code: resp.errorCode)
|
// print(string)
|
||||||
|
// }
|
||||||
|
// print("================================")
|
||||||
|
|
||||||
|
do {
|
||||||
|
let resp = try JSONDecoder().decode(Response<T>.self, from: data)
|
||||||
|
if resp.success {
|
||||||
|
continuation.resume(returning: resp.data!)
|
||||||
|
} else {
|
||||||
|
if let code = resp.errorCode {
|
||||||
|
continuation.resume(throwing: ApiError(code: code))
|
||||||
|
} else if let errorMessage = resp.error {
|
||||||
|
continuation.resume(throwing: ApiError.message(errorMessage))
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: ApiError.generic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch let error as Swift.DecodingError {
|
||||||
|
continuation.resume(throwing: ApiError.message((error as CustomDebugStringConvertible).debugDescription))
|
||||||
|
} catch {
|
||||||
|
continuation.resume(throwing: error)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw ApiError(httpStatus: httpResponse.statusCode)
|
|
||||||
}
|
}
|
||||||
} catch let error as Swift.DecodingError {
|
|
||||||
throw CocoaError.error((error as CustomDebugStringConvertible).debugDescription)
|
task.resume()
|
||||||
} catch {
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,6 +125,7 @@ public class Api: ApiProtocol {
|
|||||||
|
|
||||||
// MARK: - AutoCat public API
|
// MARK: - AutoCat public API
|
||||||
|
|
||||||
|
@MainActor
|
||||||
public func login(email: String, password: String) async throws -> User {
|
public func login(email: String, password: String) async throws -> User {
|
||||||
|
|
||||||
let body = [
|
let body = [
|
||||||
@ -122,7 +135,19 @@ public class Api: ApiProtocol {
|
|||||||
|
|
||||||
return try await self.makeBodyRequest(api: "user/login", body: body)
|
return try await self.makeBodyRequest(api: "user/login", body: body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public func signup(email: String, password: String) async throws -> User {
|
||||||
|
|
||||||
|
let body = [
|
||||||
|
"email": email,
|
||||||
|
"password": password
|
||||||
|
]
|
||||||
|
|
||||||
|
return try await self.makeBodyRequest(api: "user/signup", body: body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
public func check(plateNumber: String, force: Bool = false) async throws -> Vehicle {
|
public func check(plateNumber: String, force: Bool = false) async throws -> Vehicle {
|
||||||
|
|
||||||
var body = [
|
var body = [
|
||||||
31
AutoCatCore/Utils/ApiError.swift
Normal file
31
AutoCatCore/Utils/ApiError.swift
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum ApiError: LocalizedError {
|
||||||
|
|
||||||
|
case generic
|
||||||
|
case message(String)
|
||||||
|
case httpError(Int)
|
||||||
|
case invalidLoginOrPassword
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .generic:
|
||||||
|
return "Something bad happened"
|
||||||
|
case .message(let message):
|
||||||
|
return message
|
||||||
|
case .httpError(let status):
|
||||||
|
return "General http error (status \(status))"
|
||||||
|
case .invalidLoginOrPassword:
|
||||||
|
return "Invalid login or password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(code: Int) {
|
||||||
|
switch code {
|
||||||
|
case 0:
|
||||||
|
self = .invalidLoginOrPassword
|
||||||
|
default:
|
||||||
|
self = .generic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
AutoCatCoreTests/AutoCatCoreTests.swift
Normal file
36
AutoCatCoreTests/AutoCatCoreTests.swift
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// AutoCatCoreTests.swift
|
||||||
|
// AutoCatCoreTests
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 05.03.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import AutoCatCore
|
||||||
|
|
||||||
|
class AutoCatCoreTests: XCTestCase {
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
func testExample() throws {
|
||||||
|
// This is an example of a functional test case.
|
||||||
|
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||||
|
// Any test you write for XCTest can be annotated as throws and async.
|
||||||
|
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
|
||||||
|
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPerformanceExample() throws {
|
||||||
|
// This is an example of a performance test case.
|
||||||
|
self.measure {
|
||||||
|
// Put the code you want to measure the time of here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "display-p3",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.000",
|
|
||||||
"green" : "0.000",
|
|
||||||
"red" : "1.000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "display-p3",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.182",
|
|
||||||
"green" : "0.225",
|
|
||||||
"red" : "1.000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
//
|
|
||||||
// AutoCat2App.swift
|
|
||||||
// Shared
|
|
||||||
//
|
|
||||||
// Created by Selim Mustafaev on 04.07.2021.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct AutoCat2App: App {
|
|
||||||
let storageService = StorageService.shared
|
|
||||||
|
|
||||||
var body: some Scene {
|
|
||||||
WindowGroup {
|
|
||||||
ContentView()
|
|
||||||
.environment(\.managedObjectContext, storageService.context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
enum AlertMessage: Identifiable {
|
|
||||||
case info(title: String, body: String)
|
|
||||||
case error(error: Error)
|
|
||||||
|
|
||||||
var id: Int {
|
|
||||||
switch self {
|
|
||||||
case .info(let title, let body):
|
|
||||||
return title.hashValue + body.hashValue
|
|
||||||
case .error(let error):
|
|
||||||
return error.localizedDescription.hashValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Alert {
|
|
||||||
init(_ message: AlertMessage) {
|
|
||||||
switch message {
|
|
||||||
case .info(let title, let body):
|
|
||||||
self.init(title: Text(title), message: Text(body))
|
|
||||||
case .error(let error):
|
|
||||||
let msg = (error as NSError).displayMessage
|
|
||||||
self.init(title: Text(msg.title), message: Text(msg.body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
import CoreLocation
|
|
||||||
|
|
||||||
extension NSError {
|
|
||||||
public 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: "")
|
|
||||||
// }
|
|
||||||
|
|
||||||
if let recovery = self.localizedRecoverySuggestion {
|
|
||||||
return (title: "Error", body: self.localizedDescription + "\n" + recovery)
|
|
||||||
} else {
|
|
||||||
return (title: "Error", body: self.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CocoaError {
|
|
||||||
public static func error(_ description: String) -> NSError {
|
|
||||||
return error(Code(rawValue: 0), userInfo: [NSLocalizedDescriptionKey: description], url: nil) as NSError
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func error(_ error: String, reason: String, suggestion: String? = nil) -> NSError {
|
|
||||||
var info = [
|
|
||||||
NSLocalizedFailureErrorKey: error,
|
|
||||||
NSLocalizedFailureReasonErrorKey: reason
|
|
||||||
]
|
|
||||||
|
|
||||||
if let suggestion = suggestion {
|
|
||||||
info[NSLocalizedRecoverySuggestionErrorKey] = suggestion
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.error(Code(rawValue: 0), userInfo: info, url: nil) as NSError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CLError.Code: CustomStringConvertible {
|
|
||||||
public var description: String {
|
|
||||||
switch self {
|
|
||||||
case .locationUnknown: return "Location unknown"
|
|
||||||
case .denied: return "Access denied"
|
|
||||||
case .network: return "general, network-related error"
|
|
||||||
case .headingFailure: return "heading could not be determined"
|
|
||||||
case .regionMonitoringDenied: return "Location region monitoring has been denied"
|
|
||||||
case .regionMonitoringSetupDelayed: return "CL could not immediately initialize region monitoring"
|
|
||||||
case .regionMonitoringResponseDelayed: return "While events for this fence will be delivered, delivery will not occur immediately"
|
|
||||||
case .geocodeFoundNoResult: return "A geocode request yielded no result"
|
|
||||||
case .geocodeFoundPartialResult: return "A geocode request yielded a partial result"
|
|
||||||
case .geocodeCanceled: return "A geocode request was cancelled"
|
|
||||||
case .deferredFailed: return "Deferred mode failed"
|
|
||||||
case .deferredNotUpdatingLocation: return "Deferred mode failed because location updates disabled or paused"
|
|
||||||
case .deferredAccuracyTooLow: return "Deferred mode not supported for the requested accuracy"
|
|
||||||
case .deferredDistanceFiltered: return "Deferred mode does not support distance filters"
|
|
||||||
case .deferredCanceled: return "Deferred mode request canceled a previous request"
|
|
||||||
case .rangingUnavailable: return "Ranging cannot be performed"
|
|
||||||
case .rangingFailure: return "General ranging failure"
|
|
||||||
case .promptDeclined: return "Authorization request not presented to user"
|
|
||||||
default: return "Unknown location error (\(self.rawValue)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
//
|
|
||||||
// Persistence.swift
|
|
||||||
// Shared
|
|
||||||
//
|
|
||||||
// Created by Selim Mustafaev on 04.07.2021.
|
|
||||||
//
|
|
||||||
|
|
||||||
import CoreData
|
|
||||||
|
|
||||||
struct PersistenceController {
|
|
||||||
static let shared = PersistenceController()
|
|
||||||
|
|
||||||
static var preview: PersistenceController = {
|
|
||||||
let result = PersistenceController(inMemory: true)
|
|
||||||
let viewContext = result.container.viewContext
|
|
||||||
// for _ in 0..<10 {
|
|
||||||
// let newItem = Item(context: viewContext)
|
|
||||||
// newItem.timestamp = Date()
|
|
||||||
// }
|
|
||||||
do {
|
|
||||||
try viewContext.save()
|
|
||||||
} catch {
|
|
||||||
// Replace this implementation with code to handle the error appropriately.
|
|
||||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
|
||||||
let nsError = error as NSError
|
|
||||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}()
|
|
||||||
|
|
||||||
let container: NSPersistentCloudKitContainer
|
|
||||||
|
|
||||||
init(inMemory: Bool = false) {
|
|
||||||
container = NSPersistentCloudKitContainer(name: "AutoCat2")
|
|
||||||
if inMemory {
|
|
||||||
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
|
||||||
}
|
|
||||||
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
|
||||||
if let error = error as NSError? {
|
|
||||||
// Replace this implementation with code to handle the error appropriately.
|
|
||||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
|
||||||
|
|
||||||
/*
|
|
||||||
Typical reasons for an error here include:
|
|
||||||
* The parent directory does not exist, cannot be created, or disallows writing.
|
|
||||||
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
|
|
||||||
* The device is out of space.
|
|
||||||
* The store could not be migrated to the current model version.
|
|
||||||
Check the error message to determine what the actual problem was.
|
|
||||||
*/
|
|
||||||
fatalError("Unresolved error \(error), \(error.userInfo)")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
public enum ApiErrorCode: Int, CustomStringConvertible, Codable {
|
|
||||||
case invalidLoginOrPassword = 0
|
|
||||||
|
|
||||||
public var description: String {
|
|
||||||
switch self {
|
|
||||||
case .invalidLoginOrPassword: return "Invalid login or password"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ApiError: LocalizedError {
|
|
||||||
public let httpStatus: Int
|
|
||||||
public let message: String?
|
|
||||||
public let code: ApiErrorCode?
|
|
||||||
|
|
||||||
init(httpStatus: Int, message: String? = nil, code: ApiErrorCode? = nil) {
|
|
||||||
self.httpStatus = httpStatus
|
|
||||||
self.message = message
|
|
||||||
self.code = code
|
|
||||||
}
|
|
||||||
|
|
||||||
public var errorDescription: String? {
|
|
||||||
if let code = code {
|
|
||||||
return code.description
|
|
||||||
}
|
|
||||||
|
|
||||||
if let message = message {
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
return "General http error (status \(self.httpStatus))"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
public class AuthVM: ObservableObject {
|
|
||||||
|
|
||||||
private let api: ApiProtocol
|
|
||||||
private var settings: SettingsProtocol
|
|
||||||
|
|
||||||
init(api: ApiProtocol = Api.shared, settings: SettingsProtocol = Settings.shared) {
|
|
||||||
|
|
||||||
self.api = api
|
|
||||||
self.settings = settings
|
|
||||||
}
|
|
||||||
|
|
||||||
public func login(user: String, password: String) async throws {
|
|
||||||
settings.user = try await api.login(email: user, password: password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ACProgressView: View {
|
|
||||||
@State var isDeterminate: Bool = false
|
|
||||||
@State var progress: CGFloat = 0
|
|
||||||
@State var text: String? = nil
|
|
||||||
@State var isAnimating: Bool = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack(alignment: .center) {
|
|
||||||
Color.black.opacity(0.4)
|
|
||||||
VStack {
|
|
||||||
VStack(spacing: 24) {
|
|
||||||
if self.isDeterminate {
|
|
||||||
ZStack {
|
|
||||||
Circle()
|
|
||||||
.stroke(Color.secondary.opacity(0.2), style: StrokeStyle(lineWidth: 4))
|
|
||||||
.frame(width: 100, height: 100)
|
|
||||||
Circle()
|
|
||||||
.trim(from: 0, to: self.progress)
|
|
||||||
.stroke(Color.blue, style: StrokeStyle(lineWidth: 4))
|
|
||||||
.rotationEffect(.degrees(-90))
|
|
||||||
.frame(width: 100, height: 100)
|
|
||||||
Text("\(Int(self.progress*100)) %")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let gradient = AngularGradient(
|
|
||||||
gradient: Gradient(colors: [Color.blue, Color.blue.opacity(0.01)]),
|
|
||||||
center: .center,
|
|
||||||
startAngle: .degrees(360),
|
|
||||||
endAngle: .degrees(0))
|
|
||||||
Circle()
|
|
||||||
.stroke(gradient, style: StrokeStyle(lineWidth: 4))
|
|
||||||
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0))
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
|
|
||||||
self.isAnimating = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onDisappear(perform: {
|
|
||||||
self.isAnimating = false
|
|
||||||
})
|
|
||||||
.frame(width: 100, height: 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let msg = self.text {
|
|
||||||
Text(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(28)
|
|
||||||
}
|
|
||||||
//.frame(width: 160, height: 160)
|
|
||||||
.background(.regularMaterial)
|
|
||||||
.cornerRadius(20)
|
|
||||||
}
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ACProgressView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
Group {
|
|
||||||
ACProgressView()
|
|
||||||
ACProgressView(isDeterminate: true, progress: 0.3, text: "Loading...")
|
|
||||||
.preferredColorScheme(.dark)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct AuthView: View {
|
|
||||||
|
|
||||||
enum Field {
|
|
||||||
case email
|
|
||||||
case password
|
|
||||||
}
|
|
||||||
|
|
||||||
@ObservedObject var viewModel = AuthVM()
|
|
||||||
|
|
||||||
@State private var login: String = ""
|
|
||||||
@State private var password: String = ""
|
|
||||||
@State private var showProgress: Bool = false
|
|
||||||
@State private var alert: AlertMessage? = nil
|
|
||||||
@FocusState private var focus: Field?
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
VStack(alignment: .center, spacing: 16) {
|
|
||||||
Spacer()
|
|
||||||
#if os(iOS)
|
|
||||||
TextField("Login", text: $login)
|
|
||||||
.focused($focus, equals: .email)
|
|
||||||
.textContentType(.emailAddress)
|
|
||||||
.keyboardType(.emailAddress)
|
|
||||||
#else
|
|
||||||
TextField("Login", text: $login)
|
|
||||||
.focused($focus, equals: .email)
|
|
||||||
#endif
|
|
||||||
SecureField("Password", text: $password)
|
|
||||||
.focused($focus, equals: .password)
|
|
||||||
Button("Login") {
|
|
||||||
Task.init {
|
|
||||||
do {
|
|
||||||
self.focus = nil
|
|
||||||
self.showProgress = true
|
|
||||||
try await self.viewModel.login(user: self.login, password: self.password)
|
|
||||||
self.showProgress = false
|
|
||||||
} catch {
|
|
||||||
self.showProgress = false
|
|
||||||
self.alert = .error(error: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alert(item: $alert, content: Alert.init)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.large)
|
|
||||||
.textFieldStyle(.roundedBorder)
|
|
||||||
.padding(20)
|
|
||||||
|
|
||||||
if self.showProgress {
|
|
||||||
ACProgressView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AuthView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
Group {
|
|
||||||
AuthView()
|
|
||||||
.previewDevice("iPhone 8")
|
|
||||||
AuthView()
|
|
||||||
.preferredColorScheme(.dark)
|
|
||||||
.previewDevice("iPhone 8")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct CheckView: View {
|
|
||||||
@State private var number: String = ""
|
|
||||||
@FetchRequest(entity: CDVehicle.entity(), sortDescriptors: []) var vehicles: FetchedResults<CDVehicle>
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
VStack(alignment: .center, spacing: 16) {
|
|
||||||
TextField("A123AA777", text: $number)
|
|
||||||
Button("Check") {
|
|
||||||
|
|
||||||
}
|
|
||||||
List(vehicles) { vehicle in
|
|
||||||
Text(vehicle.number ?? "<none>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .primaryAction) {
|
|
||||||
Button("New") {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#if os(iOS)
|
|
||||||
.navigationBarTitle("Title", displayMode: .inline)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CheckView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
CheckView()
|
|
||||||
.preferredColorScheme(.dark)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import CoreData
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
@StateObject var settings = Settings.shared
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
if settings.user.token.isEmpty {
|
|
||||||
AuthView()
|
|
||||||
} else {
|
|
||||||
MainView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
ContentView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct MainView: View {
|
|
||||||
var body: some View {
|
|
||||||
#if os(iOS)
|
|
||||||
MainViewSmall()
|
|
||||||
#else
|
|
||||||
MainViewBig()
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MainView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
MainView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct MainViewSmall: View {
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
TabView {
|
|
||||||
CheckView()
|
|
||||||
.tabItem {
|
|
||||||
Image(systemName: "eye")
|
|
||||||
Text("Check")
|
|
||||||
}
|
|
||||||
RecordsView()
|
|
||||||
.tabItem {
|
|
||||||
Image(systemName: "recordingtape")
|
|
||||||
Text("Records")
|
|
||||||
}
|
|
||||||
SearchView()
|
|
||||||
.tabItem {
|
|
||||||
Image(systemName: "magnifyingglass")
|
|
||||||
Text("Search")
|
|
||||||
}
|
|
||||||
SettingsView()
|
|
||||||
.tabItem {
|
|
||||||
Image(systemName: "gear")
|
|
||||||
Text("Settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#if os(iOS)
|
|
||||||
.navigationBarHidden(true)
|
|
||||||
#endif
|
|
||||||
Text("detail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MainViewSmall_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
Group {
|
|
||||||
MainViewSmall()
|
|
||||||
MainViewSmall()
|
|
||||||
.previewInterfaceOrientation(.landscapeLeft)
|
|
||||||
.previewDevice("iPad Pro (9.7-inch)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
//
|
|
||||||
// PlateNumberView.swift
|
|
||||||
// AutoCat2
|
|
||||||
//
|
|
||||||
// Created by Selim Mustafaev on 21.11.2021.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct PlateNumberView: View {
|
|
||||||
|
|
||||||
let number: PlateNumber
|
|
||||||
let unrecognized: Bool
|
|
||||||
let outdated: Bool
|
|
||||||
|
|
||||||
private var fgColor: Color {
|
|
||||||
if unrecognized {
|
|
||||||
return Color("PlateBackgroundError")
|
|
||||||
} else {
|
|
||||||
return Color("PlateForeground")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
RoundedRectangle(cornerRadius: 6)
|
|
||||||
.fill(fgColor)
|
|
||||||
GeometryReader { geometry in
|
|
||||||
HStack(alignment: .center, spacing: 2) {
|
|
||||||
ZStack {
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.fill(Color("PlateBackground"))
|
|
||||||
Text(number.mainPart())
|
|
||||||
.font(Font.custom("RoadNumbers", size: geometry.size.height*0.9))
|
|
||||||
}
|
|
||||||
.frame(width: geometry.size.width*0.73 - 1)
|
|
||||||
ZStack {
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.fill(Color("PlateBackground"))
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
Text(number.region())
|
|
||||||
.frame(height: geometry.size.height*0.65, alignment: .center)
|
|
||||||
HStack {
|
|
||||||
|
|
||||||
}
|
|
||||||
.frame(height: geometry.size.height*0.35, alignment: .center)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(width: geometry.size.width*0.27 - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(2)
|
|
||||||
}
|
|
||||||
.aspectRatio(520.0/112.0, contentMode: .fit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PlateNumberView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
Group {
|
|
||||||
PlateNumberView(number: PlateNumber("Е201АМ761"), unrecognized: false, outdated: false)
|
|
||||||
PlateNumberView(number: PlateNumber("Е201АМ761"), unrecognized: true, outdated: false)
|
|
||||||
PlateNumberView(number: PlateNumber("Е201АМ761"), unrecognized: false, outdated: true)
|
|
||||||
}
|
|
||||||
.previewLayout(.fixed(width: 200, height: 50))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct RecordsView: View {
|
|
||||||
var body: some View {
|
|
||||||
Text("Records view")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RecordsView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
RecordsView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ReportView: View {
|
|
||||||
var body: some View {
|
|
||||||
Text("Report view")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ReportView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
ReportView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct SearchView: View {
|
|
||||||
var body: some View {
|
|
||||||
Text("Search view")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SearchView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
SearchView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct SettingsView: View {
|
|
||||||
var body: some View {
|
|
||||||
Text("Search view")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SettingsView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
SettingsView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
//
|
|
||||||
// CheckNumberPopup.swift
|
|
||||||
// AutoCat2 (macOS)
|
|
||||||
//
|
|
||||||
// Created by Selim Mustafaev on 07.11.2021.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct CheckNumberPopup: View {
|
|
||||||
@Environment(\.presentationMode) var presentation
|
|
||||||
@State var plateNumber: String = ""
|
|
||||||
@State var progress = false
|
|
||||||
@State private var alert: AlertMessage? = nil
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .center, spacing: 8) {
|
|
||||||
Text("Check new plate number")
|
|
||||||
TextField("qwe", text: $plateNumber, prompt: Text("asdf"))
|
|
||||||
.disabled(progress)
|
|
||||||
Spacer()
|
|
||||||
if progress {
|
|
||||||
ProgressView()
|
|
||||||
.progressViewStyle(CircularProgressViewStyle())
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
HStack {
|
|
||||||
Button("Cancel") {
|
|
||||||
self.presentation.wrappedValue.dismiss()
|
|
||||||
}
|
|
||||||
.disabled(progress)
|
|
||||||
Button("Check") {
|
|
||||||
Task.init {
|
|
||||||
progress = true
|
|
||||||
do {
|
|
||||||
try await VehicleService.shared.check(plateNumber: plateNumber.uppercased(), force: false)
|
|
||||||
} catch {
|
|
||||||
alert = .error(error: error)
|
|
||||||
}
|
|
||||||
progress = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alert(item: $alert, content: Alert.init)
|
|
||||||
.disabled(progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.frame(minWidth: 300, minHeight: 180)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CheckNumberPopup_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
CheckNumberPopup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct MainViewBig: View {
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
SidebarView()
|
|
||||||
Text("Master")
|
|
||||||
Text("Detail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SidebarView: View {
|
|
||||||
@State var selection: String?
|
|
||||||
@State private var showAddPlateNumberView = false
|
|
||||||
|
|
||||||
@FetchRequest(entity: CDVehicle.entity(), sortDescriptors: []) var vehicles: FetchedResults<CDVehicle>
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
List(selection: $selection) {
|
|
||||||
Section("History") {
|
|
||||||
NavigationLink(destination: VehiclesListView(vehicles: vehicles)) {
|
|
||||||
Label("All", systemImage: "car.2")
|
|
||||||
.badge(vehicles.count)
|
|
||||||
}
|
|
||||||
NavigationLink(destination: VehiclesListView(vehicles: vehicles.filter(\.unrecognized))) {
|
|
||||||
Label("Unreconized", systemImage: "eye.slash")
|
|
||||||
.badge(vehicles.filter(\.unrecognized).count)
|
|
||||||
}
|
|
||||||
NavigationLink(destination: VehiclesListView(vehicles: vehicles.filter(\.outdated))) {
|
|
||||||
Label("Outdated", systemImage: "wind")
|
|
||||||
.badge(vehicles.filter(\.outdated).count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.collapsible(false)
|
|
||||||
|
|
||||||
Section("Other") {
|
|
||||||
Label("Recordings", systemImage: "waveform.path")
|
|
||||||
Label("Search", systemImage: "magnifyingglass")
|
|
||||||
}
|
|
||||||
.collapsible(false)
|
|
||||||
}
|
|
||||||
.frame(minWidth: 180)
|
|
||||||
.sheet(isPresented: $showAddPlateNumberView, onDismiss: {
|
|
||||||
print("Dismiss")
|
|
||||||
}, content: {
|
|
||||||
CheckNumberPopup()
|
|
||||||
})
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem {
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
ToolbarItem {
|
|
||||||
Button {
|
|
||||||
self.showAddPlateNumberView = true
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct VehiclesListView<VehicleCollection>: View where VehicleCollection: RandomAccessCollection, VehicleCollection.Element == CDVehicle {
|
|
||||||
|
|
||||||
var vehicles: VehicleCollection
|
|
||||||
@State var selection: CDVehicle?
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
List(selection: $selection) {
|
|
||||||
ForEach(vehicles, id: \.self) { vehicle in
|
|
||||||
let number = PlateNumber(vehicle.number ?? "")
|
|
||||||
PlateNumberView(number: number, unrecognized: vehicle.unrecognized, outdated: vehicle.outdated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MainViewBig_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
MainViewBig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import XCTest
|
|
||||||
|
|
||||||
class Tests_iOS: XCTestCase {
|
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
|
||||||
continueAfterFailure = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
|
||||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
|
||||||
}
|
|
||||||
|
|
||||||
func testExample() throws {
|
|
||||||
// UI tests must launch the application that they test.
|
|
||||||
let app = XCUIApplication()
|
|
||||||
app.launch()
|
|
||||||
|
|
||||||
// Use recording to get started writing UI tests.
|
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user