Removing Kingfisher package.

Adding License Plate view.
Adding stub for new SwiftUI history screen.
This commit is contained in:
Selim Mustafaev 2025-01-15 22:11:08 +03:00
parent 57c222b937
commit 67977f1ebf
13 changed files with 252 additions and 386 deletions

View File

@ -30,6 +30,9 @@
7A11470D23FDE7E600B424AF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7A11470B23FDE7E600B424AF /* LaunchScreen.storyboard */; };
7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11471523FDEB2A00B424AF /* MainSplitController.swift */; };
7A11471A23FE839000B424AF /* AuthController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11471923FE839000B424AF /* AuthController.swift */; };
7A131FD32D37B75500DC7755 /* HistoryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A131FD22D37B75500DC7755 /* HistoryScreen.swift */; };
7A131FD52D37B76A00DC7755 /* HistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A131FD42D37B76A00DC7755 /* HistoryViewModel.swift */; };
7A131FD72D37B77E00DC7755 /* HistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A131FD62D37B77E00DC7755 /* HistoryCoordinator.swift */; };
7A1441662C297EDE00E79018 /* NotesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1441652C297EDE00E79018 /* NotesScreen.swift */; };
7A1441682C297EFD00E79018 /* NotesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1441672C297EFD00E79018 /* NotesViewModel.swift */; };
7A14416C2C297F2100E79018 /* NotesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A14416B2C297F2100E79018 /* NotesCoordinator.swift */; };
@ -115,7 +118,6 @@
7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A71580D2C4445A200852088 /* AdsCoordinator.swift */; };
7A7158122C444A6400852088 /* AdsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158112C444A6400852088 /* AdsViewModel.swift */; };
7A71EF572D0A26B200943129 /* EventModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A71EF562D0A26B200943129 /* EventModel.swift */; };
7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */; };
7A761C042677F18E0005F28F /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474323FF06CA00B424AF /* ApiService.swift */; };
7A761C052677F1BC0005F28F /* CocoaError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF824A09CAD0035F39E /* CocoaError.swift */; };
7A761C07267E8E7F0005F28F /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A15051124DB3E3000F39631 /* AnyEncodable.swift */; };
@ -124,8 +126,8 @@
7A761C0B267E8FF90005F28F /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A761C0A267E8FF90005F28F /* Error.swift */; };
7A813DC32508EE4F00CC93B9 /* EventCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A813DC22508EE4F00CC93B9 /* EventCell.swift */; };
7A8A2209248D10EC0073DFD9 /* ResizeImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */; };
7A8A220B248D67B60073DFD9 /* VehicleReportImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A220A248D67B60073DFD9 /* VehicleReportImage.swift */; };
7A8AB76525A0DB8F00ECF2C1 /* BundleVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8AB76425A0DB8F00ECF2C1 /* BundleVersion.swift */; };
7A912F372D381B7400002938 /* LicensePlateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A912F362D381B7400002938 /* LicensePlateView.swift */; };
7A91894F29A2BD8700519C74 /* GestureRecognizers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A91894E29A2BD8700519C74 /* GestureRecognizers.swift */; };
7A961C6C2C4C3C8600CE2211 /* TextRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A961C6B2C4C3C8600CE2211 /* TextRowView.swift */; };
7A961C6E2C4C3C9E00CE2211 /* LinkRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A961C6D2C4C3C9E00CE2211 /* LinkRowView.swift */; };
@ -134,7 +136,6 @@
7A99406426E4BFAE002E9CB6 /* VehicleNoteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A99406326E4BFAE002E9CB6 /* VehicleNoteCell.swift */; };
7A9FEEC82529AB23001CA50E /* RxRealmDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */; };
7AA514E02D0B75B3001CAC50 /* StorageService+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA514DF2D0B75B3001CAC50 /* StorageService+Events.swift */; };
7AA7BC3325A5DFB80053A5D5 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 7AF58D332402A91C00CE01A0 /* Kingfisher */; };
7AA7BC3525A5DFB80053A5D5 /* ExceptionCatcher in Frameworks */ = {isa = PBXBuildFile; productRef = 7A813DC02508C4D900CC93B9 /* ExceptionCatcher */; };
7AA7BC3625A5DFB80053A5D5 /* PKHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABDE1C2532F3EB0041AFC6 /* PKHUD */; };
7AAAFAD32C4D0FD00050410D /* ACImageSliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAAFAD22C4D0FD00050410D /* ACImageSliderView.swift */; };
@ -289,6 +290,9 @@
7A11474623FF2AA500B424AF /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
7A11474823FF2B2D00B424AF /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; };
7A11474D23FFEE8800B424AF /* SVProgressHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SVProgressHUD.framework; path = Carthage/Build/iOS/SVProgressHUD.framework; sourceTree = "<group>"; };
7A131FD22D37B75500DC7755 /* HistoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryScreen.swift; sourceTree = "<group>"; };
7A131FD42D37B76A00DC7755 /* HistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewModel.swift; sourceTree = "<group>"; };
7A131FD62D37B77E00DC7755 /* HistoryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinator.swift; sourceTree = "<group>"; };
7A1441652C297EDE00E79018 /* NotesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesScreen.swift; sourceTree = "<group>"; };
7A1441672C297EFD00E79018 /* NotesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesViewModel.swift; sourceTree = "<group>"; };
7A14416B2C297F2100E79018 /* NotesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesCoordinator.swift; sourceTree = "<group>"; };
@ -387,14 +391,13 @@
7A71580D2C4445A200852088 /* AdsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsCoordinator.swift; sourceTree = "<group>"; };
7A7158112C444A6400852088 /* AdsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsViewModel.swift; sourceTree = "<group>"; };
7A71EF562D0A26B200943129 /* EventModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventModel.swift; sourceTree = "<group>"; };
7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclePhotoCell.swift; sourceTree = "<group>"; };
7A761C0A267E8FF90005F28F /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = "<group>"; };
7A813DBD2506A57100CC93B9 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/AuthenticationServices.framework; sourceTree = DEVELOPER_DIR; };
7A813DC22508EE4F00CC93B9 /* EventCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCell.swift; sourceTree = "<group>"; };
7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeImage.swift; sourceTree = "<group>"; };
7A8A220A248D67B60073DFD9 /* VehicleReportImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleReportImage.swift; sourceTree = "<group>"; };
7A8AB76425A0DB8F00ECF2C1 /* BundleVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleVersion.swift; sourceTree = "<group>"; };
7A8AB76725A0DC8200ECF2C1 /* DebugInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugInfo.swift; sourceTree = "<group>"; };
7A912F362D381B7400002938 /* LicensePlateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicensePlateView.swift; sourceTree = "<group>"; };
7A91894E29A2BD8700519C74 /* GestureRecognizers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureRecognizers.swift; sourceTree = "<group>"; };
7A92D0AB240425B100EF3B77 /* ATGMediaBrowser.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ATGMediaBrowser.framework; path = Carthage/Build/iOS/ATGMediaBrowser.framework; sourceTree = "<group>"; };
7A961C6B2C4C3C8600CE2211 /* TextRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRowView.swift; sourceTree = "<group>"; };
@ -468,7 +471,6 @@
buildActionMask = 2147483647;
files = (
7AA7BC3525A5DFB80053A5D5 /* ExceptionCatcher in Frameworks */,
7AA7BC3325A5DFB80053A5D5 /* Kingfisher in Frameworks */,
7ADF23062C25B5BF002624FF /* RealmSwift in Frameworks */,
7AA7BC3625A5DFB80053A5D5 /* PKHUD in Frameworks */,
7AC3554A2969652F00889457 /* SwiftEntryKit in Frameworks */,
@ -670,9 +672,20 @@
name = Frameworks;
sourceTree = "<group>";
};
7A131FD12D37B74100DC7755 /* HistoryScreen */ = {
isa = PBXGroup;
children = (
7A131FD22D37B75500DC7755 /* HistoryScreen.swift */,
7A131FD42D37B76A00DC7755 /* HistoryViewModel.swift */,
7A131FD62D37B77E00DC7755 /* HistoryCoordinator.swift */,
);
path = HistoryScreen;
sourceTree = "<group>";
};
7A1441632C297E9800E79018 /* Screens */ = {
isa = PBXGroup;
children = (
7A131FD12D37B74100DC7755 /* HistoryScreen */,
7AB9FE202D08C28E005DE374 /* EventsScreen */,
7ABD1B452D044A0900B43213 /* GalleryScreen */,
7A1E78F42CE9001A0004B740 /* ReportScreen */,
@ -729,7 +742,6 @@
7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */,
7A659B5A24A3768A0043A0F2 /* Substrings.swift */,
7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */,
7A8A220A248D67B60073DFD9 /* VehicleReportImage.swift */,
7A761C0A267E8FF90005F28F /* Error.swift */,
7AC76D7A270083AE0084DB27 /* TextView.swift */,
);
@ -761,7 +773,6 @@
isa = PBXGroup;
children = (
7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */,
7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */,
7A1090E724A394F100B4F0B2 /* AudioRecordCell.swift */,
7A813DC22508EE4F00CC93B9 /* EventCell.swift */,
7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */,
@ -1070,6 +1081,7 @@
7A2E11282CCE395300E5CA17 /* OptionalDatePicker.swift */,
7A4927D42CCE438600851C01 /* OptionalBinding.swift */,
7AABBE3A2CF9F85600346588 /* Binding+Map.swift */,
7A912F362D381B7400002938 /* LicensePlateView.swift */,
);
path = SwiftUI;
sourceTree = "<group>";
@ -1115,7 +1127,6 @@
);
name = AutoCat;
packageProductDependencies = (
7AF58D332402A91C00CE01A0 /* Kingfisher */,
7A813DC02508C4D900CC93B9 /* ExceptionCatcher */,
7AABDE1C2532F3EB0041AFC6 /* PKHUD */,
7AC355492969652F00889457 /* SwiftEntryKit */,
@ -1236,7 +1247,6 @@
);
mainGroup = 7A1146F423FDE7E500B424AF;
packageReferences = (
7AF58D322402A91C00CE01A0 /* XCRemoteSwiftPackageReference "Kingfisher" */,
7A05160F241412CA00FC55AC /* XCRemoteSwiftPackageReference "SwiftDate" */,
7A813DBF2508C4D900CC93B9 /* XCRemoteSwiftPackageReference "ExceptionCatcher" */,
7AABDE1B2532F3EB0041AFC6 /* XCRemoteSwiftPackageReference "PKHUD" */,
@ -1354,6 +1364,7 @@
7A7158072C44085600852088 /* OsagoScreen.swift in Sources */,
7ABD1B492D044A4700B43213 /* GalleryViewModel.swift in Sources */,
7AAAFAD32C4D0FD00050410D /* ACImageSliderView.swift in Sources */,
7A912F372D381B7400002938 /* LicensePlateView.swift in Sources */,
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */,
7AC76D7B270083AE0084DB27 /* TextView.swift in Sources */,
7A1090E824A394F100B4F0B2 /* AudioRecordCell.swift in Sources */,
@ -1387,11 +1398,11 @@
7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */,
7AC35554296973E100889457 /* ACButton.swift in Sources */,
7AAAFADC2C4D1E130050410D /* ACImageSliderView+Modifier.swift in Sources */,
7A8A220B248D67B60073DFD9 /* VehicleReportImage.swift in Sources */,
7AFBE8C42C302561003C491D /* ACHudContainer.swift in Sources */,
7AC3555B296995B200889457 /* UIEdgeInsets.swift in Sources */,
7A06E0AC2C7065AC005731AC /* SettingsScreen.swift in Sources */,
7A4322952CB2CD0F00085CF6 /* FiltersCoordinator.swift in Sources */,
7A131FD72D37B77E00DC7755 /* HistoryCoordinator.swift in Sources */,
7A7158002C43EA6900852088 /* OwnersScreen.swift in Sources */,
7A1441702C2998B200E79018 /* Formatters.swift in Sources */,
7A4322912CB2CC8A00085CF6 /* FiltersScreen.swift in Sources */,
@ -1401,6 +1412,7 @@
7A06E0B02C7065D8005731AC /* SettingsCoordinator.swift in Sources */,
7A91894F29A2BD8700519C74 /* GestureRecognizers.swift in Sources */,
7AFBE8CC2C3085C6003C491D /* ACProgressView.swift in Sources */,
7A131FD32D37B75500DC7755 /* HistoryScreen.swift in Sources */,
7AB9FE222D08C2A5005DE374 /* EventsScreen.swift in Sources */,
7ADF6C93250B954900F237B2 /* Navigation.swift in Sources */,
7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */,
@ -1409,8 +1421,8 @@
7ADF6C97250F41B000F237B2 /* PNKeyboard.swift in Sources */,
7A1022702C551EFD00B84627 /* LocationEditCoordinator.swift in Sources */,
7A7158042C43EAA200852088 /* OwnersCoordinator.swift in Sources */,
7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */,
7A17CE4C2A2E850200626A6E /* UISegmentedControl.swift in Sources */,
7A131FD52D37B76A00DC7755 /* HistoryViewModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -2013,14 +2025,6 @@
minimumVersion = 0.0.11;
};
};
7AF58D322402A91C00CE01A0 /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/onevcat/Kingfisher";
requirement = {
branch = "8.0.0-alpha.1";
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@ -2074,11 +2078,6 @@
package = 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */;
productName = RealmSwift;
};
7AF58D332402A91C00CE01A0 /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = 7AF58D322402A91C00CE01A0 /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
7AF8606B2CB9B20C00954D2F /* Mockable */ = {
isa = XCSwiftPackageProductDependency;
package = 7ACBB91C2CB9B155005A5168 /* XCRemoteSwiftPackageReference "Mockable" */;

View File

@ -1,5 +1,5 @@
{
"originHash" : "d6bab97967371dd248cd5b3fdb49293879398c897bea34714c48fd3d4cb90fb1",
"originHash" : "6fccb9fdc0d29647d4f0b927aef60f375302d72b5b724992eab52ac0d8ec71c3",
"pins" : [
{
"identity" : "exceptioncatcher",
@ -10,15 +10,6 @@
"version" : "1.1.0"
}
},
{
"identity" : "kingfisher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher",
"state" : {
"branch" : "8.0.0-alpha.1",
"revision" : "bb4e6ecf6c7a221dfc51c8e69f04fd3757fc519a"
}
},
{
"identity" : "mockable",
"kind" : "remoteSourceControl",

View File

@ -2,8 +2,6 @@ import UIKit
import RealmSwift
import PKHUD
import AutoCatCore
import SwiftLocation
import CoreLocation
enum QuickAction {
case none
@ -72,11 +70,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
HUD.dimsBackground = true
HUD.allowsInteraction = false
registerServices()
setupAppearance()
//Logging.URLRequests = { _ in false };
return true
}
@ -93,29 +88,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
}
func registerServices() {
let container = ServiceContainer.shared
let settingsService = SettingsService(defaults: .standard)
container.register(SettingsServiceProtocol.self, instance: settingsService)
container.register(ApiServiceProtocol.self, instance: ApiService())
let locationService = LocationService(
geocoder: CLGeocoder(),
locationManager: Location(),
settingsService: settingsService
)
container.register(LocationServiceProtocol.self, instance: locationService)
Task {
container.register(StorageServiceProtocol.self,
instance: try await StorageService(settingsService: settingsService))
}
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {

View File

@ -1,38 +0,0 @@
import UIKit
import Kingfisher
import AutoCatCore
@MainActor
class VehiclePhotoCell: UICollectionViewCell {
@IBOutlet weak var photo: UIImageView!
@IBOutlet weak var model: UILabel!
@IBOutlet weak var date: UILabel!
let formatter = DateFormatter()
override func awakeFromNib() {
super.awakeFromNib()
DispatchQueue.main.async {
self.layer.cornerRadius = 8
self.formatter.timeStyle = .none
self.formatter.dateStyle = .medium
}
}
override func prepareForReuse() {
super.prepareForReuse()
self.photo.kf.cancelDownloadTask()
}
func configure(with photoModel: VehiclePhoto) {
if let url = URL(string: photoModel.url) {
self.photo.kf.setImage(with: url)
}
self.model.text = "\(photoModel.brand ?? "") \(photoModel.model ?? "")"
let date = Date(timeIntervalSince1970: photoModel.date/1000)
self.date.text = formatter.string(from: date)
}
}

View File

@ -23,9 +23,19 @@ class MainTabController: UITabBarController, UITabBarControllerDelegate {
traitOverrides.horizontalSizeClass = .compact
}
setupHistoryTab()
Task { await addSettings() }
}
func setupHistoryTab() {
let coordinator = HistoryCoordinator()
let controller = coordinator.start()
controller.tabBarItem = UITabBarItem(title: NSLocalizedString("History", comment: ""),
image: UIImage(systemName: "clock.arrow.circlepath"), tag: 0)
viewControllers?[0] = controller
}
func addSettings() async {
let coordinator = SettingsCoordinator(tabController: self)

View File

@ -1,280 +0,0 @@
import UIKit
import Kingfisher
import AutoCatCore
extension VehicleDto {
@MainActor
func drawLine(y: CGFloat, width: CGFloat, margin: CGFloat = 15,context: CGContext) {
let lineWidth = 1/UIScreen.main.scale
context.move(to: CGPoint(x: margin, y: y + lineWidth/2))
context.addLine(to: CGPoint(x: width, y: y + lineWidth/2))
context.closePath()
context.setLineWidth(lineWidth)
context.setStrokeColor(UIColor.opaqueSeparator.cgColor)
context.strokePath()
}
@MainActor
func drawCell(y: CGFloat, width: CGFloat, height: CGFloat, title: String, value: String, lineMargin: CGFloat = 15, context: CGContext) {
let fontSize: CGFloat = 17
let font = UIFont.systemFont(ofSize: fontSize)
let offest = (height - fontSize)/2
let pStyle = NSMutableParagraphStyle()
pStyle.alignment = .right
let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: UIColor.label]
let valueAttributs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: UIColor.secondaryLabel, .paragraphStyle: pStyle]
let rect = CGRect(x: 15, y: y + offest, width: width - 30, height: height)
UIColor.secondarySystemGroupedBackground.setFill()
UIRectFill(CGRect(x: 0, y: y, width: width, height: height))
title.draw(with: rect, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
value.draw(with: rect, options: .usesLineFragmentOrigin, attributes: valueAttributs, context: nil)
self.drawLine(y: y + height - 1/UIScreen.main.scale, width: width, margin: lineMargin, context: context)
}
@MainActor
func drawBigTextCell(y: CGFloat, width: CGFloat, title: String, value: String, lineMargin: CGFloat = 15, context: CGContext) -> CGFloat {
let pStyle = NSMutableParagraphStyle()
pStyle.alignment = .right
let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 17), .foregroundColor: UIColor.label]
let valueAttributs: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 15), .foregroundColor: UIColor.secondaryLabel, .paragraphStyle: pStyle]
UIColor.secondarySystemGroupedBackground.setFill()
let text = NSMutableAttributedString(string: title + " - " + value)
text.setAttributes(attributes, range: NSRange(location: 0, length: title.count))
text.setAttributes(valueAttributs, range: NSRange(location: title.count, length: value.count + 3))
let textRect = text.boundingRect(with: CGSize(width: width - 30, height: 1000), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
let height = textRect.size.height > 28 ? textRect.size.height + 16 : 44
let offset = (height - textRect.size.height)/2
let rect = CGRect(x: 15, y: y + offset, width: width - 30, height: height)
UIRectFill(CGRect(x: 0, y: y, width: width, height: height))
text.draw(with: rect, options: .usesLineFragmentOrigin, context: nil)
self.drawLine(y: y + height - 1/UIScreen.main.scale, width: width, margin: lineMargin, context: context)
return height
}
@MainActor
func reportImage(width: CGFloat) -> UIImage {
var realHeight: CGFloat = 0
let rect = CGRect(origin: .zero, size: CGSize(width: width, height: CGFloat(10000)))
let renderer = UIGraphicsImageRenderer(bounds: rect)
let image = renderer.image { rendererContext in
let cellHeight: CGFloat = 44
let ctx = rendererContext.cgContext
let centeredStyle = NSMutableParagraphStyle()
centeredStyle.alignment = .center
let titleAttributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 20), .paragraphStyle: centeredStyle, .foregroundColor: UIColor.label]
let headerAttributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 13), .foregroundColor: UIColor.secondaryLabel]
let w: CGFloat = width
var y: CGFloat = 0
UIColor.systemGroupedBackground.setFill()
UIRectFill(rect)
y += 12
"\(self.brand?.name?.original ?? "Unknown model") (\(self.getNumber()))".draw(with: CGRect(x: 0, y: y, width: w, height: 30), options: .usesLineFragmentOrigin, attributes: titleAttributes, context: nil)
y += 50
"GENERAL".draw(with: CGRect(x: 15, y: y, width: w - 15, height: 24), options: .usesLineFragmentOrigin, attributes: headerAttributes, context: nil)
y += 24
self.drawLine(y: y, width: w, margin: 0, context: ctx)
y += 1/UIScreen.main.scale
self.drawCell(y: y, width: w, height: cellHeight, title: "Year", value: String(self.year), context: ctx)
y += cellHeight
self.drawCell(y: y, width: w, height: cellHeight, title: "Color", value: self.color ?? "<unknown>", context: ctx)
y += cellHeight
self.drawCell(y: y, width: w, height: cellHeight, title: "Category", value: self.category ?? "<unknown>", context: ctx)
y += cellHeight
var position = "Unknown"
if let rightWheel = self.isRightWheel {
position = rightWheel ? "Right" : "Left"
}
self.drawCell(y: y, width: w, height: cellHeight, title: "Steering wheel position", value: position, context: ctx)
y += cellHeight
var japanese = "<Unknown>"
if let isJapanese = self.isJapanese {
japanese = isJapanese ? "Yes" : "No"
}
self.drawCell(y: y, width: w, height: cellHeight, title: "Japanese", value: japanese, lineMargin: 0, context: ctx)
y += cellHeight + 32
"IDENTIFIERS".draw(with: CGRect(x: 15, y: y, width: w - 15, height: 24), options: .usesLineFragmentOrigin, attributes: headerAttributes, context: nil)
y += 24
self.drawLine(y: y, width: w, margin: 0, context: ctx)
y += 1/UIScreen.main.scale
self.drawCell(y: y, width: w, height: cellHeight, title: "Plate number", value: self.getNumber(), context: ctx)
y += cellHeight
self.drawCell(y: y, width: w, height: cellHeight, title: "VIN", value: self.vin1 ?? "<unknown>", context: ctx)
y += cellHeight
self.drawCell(y: y, width: w, height: cellHeight, title: "STS", value: self.sts ?? "<unknown>", context: ctx)
y += cellHeight
self.drawCell(y: y, width: w, height: cellHeight, title: "PTS", value: self.pts ?? "<unknown>", context: ctx)
y += cellHeight + 32
"ENGINE".draw(with: CGRect(x: 15, y: y, width: w - 15, height: 24), options: .usesLineFragmentOrigin, attributes: headerAttributes, context: nil)
y += 24
self.drawLine(y: y, width: w, margin: 0, context: ctx)
y += 1/UIScreen.main.scale
self.drawCell(y: y, width: w, height: cellHeight, title: "Number", value: self.engine?.number ?? "<unknown>", context: ctx)
y += cellHeight
self.drawCell(y: y, width: w, height: cellHeight, title: "Fuel type", value: self.engine?.fuelType ?? "<unknown>", context: ctx)
y += cellHeight
self.drawCell(y: y, width: w, height: cellHeight, title: "Volume (cm³)", value: String(self.engine?.volume ?? 0), context: ctx)
y += cellHeight
self.drawCell(y: y, width: w, height: cellHeight, title: "Power (HP)", value: String(self.engine?.powerHp ?? 0), context: ctx)
y += cellHeight
self.drawCell(y: y, width: w, height: cellHeight, title: "Power (kw)", value: String(self.engine?.powerKw ?? 0), context: ctx)
y += cellHeight + 32
"OWNERSHIP PERIODS (\(self.ownershipPeriods.count))".draw(with: CGRect(x: 15, y: y, width: w - 15, height: 24), options: .usesLineFragmentOrigin, attributes: headerAttributes, context: nil)
y += 24
self.drawLine(y: y, width: w, margin: 0, context: ctx)
y += 1/UIScreen.main.scale
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .none
for period in self.ownershipPeriods {
self.drawCell(y: y, width: w, height: cellHeight, title: "Owner type", value: period.ownerType, context: ctx)
y += cellHeight
let fromDate = Date(timeIntervalSince1970: TimeInterval(period.from/1000))
self.drawCell(y: y, width: w, height: cellHeight, title: "From", value: formatter.string(from: fromDate), context: ctx)
y += cellHeight
let toDate = Date(timeIntervalSince1970: TimeInterval(period.to/1000))
let toString = period.to == 0 ? "now" : formatter.string(from: toDate)
self.drawCell(y: y, width: w, height: cellHeight, title: "To", value: toString, context: ctx)
y += cellHeight
let height = self.drawBigTextCell(y: y, width: w, title: "Last operation", value: period.lastOperation, lineMargin: 0, context: ctx)
y += height + 8
}
y += 24
if !self.events.isEmpty {
"LOCATIONS".draw(with: CGRect(x: 15, y: y, width: w - 15, height: 24), options: .usesLineFragmentOrigin, attributes: headerAttributes, context: nil)
y += 24
self.drawLine(y: y, width: w, margin: 0, context: ctx)
y += 1/UIScreen.main.scale
for event in self.events {
let date = Date(timeIntervalSince1970: event.date)
self.drawCell(y: y, width: w, height: cellHeight, title: "Date", value: formatter.string(from: date), context: ctx)
y += cellHeight
if let address = event.address {
self.drawCell(y: y, width: w, height: cellHeight, title: "Address", value: address, context: ctx)
y += cellHeight
}
let location = String(format: "%.05f, %.05f", event.latitude, event.longitude)
self.drawCell(y: y, width: w, height: cellHeight, title: "Location", value: location, context: ctx)
y += cellHeight + 8
}
}
y += 24
"PHOTOS (\(self.photos.count))".draw(with: CGRect(x: 15, y: y, width: w - 15, height: 24), options: .usesLineFragmentOrigin, attributes: headerAttributes, context: nil)
y += 24
self.drawLine(y: y, width: w, margin: 0, context: ctx)
y += 1/UIScreen.main.scale
// TODO: Fix drawing photos in report image
// for photo in self.photos {
// let date = Date(timeIntervalSince1970: TimeInterval(photo.date/1000))
// var name = "<Unknown model>"
// if let brand = photo.brand, let model = photo.model {
// name = "\(brand) \(model)"
// }
//
// if let url = URL(string: photo.url) {
// if let image = ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) {
// let imgHeight = image.size.height*w/image.size.width
// let rect = CGRect(x: 0, y: y, width: w, height: imgHeight)
// image.draw(in: rect)
// y += imgHeight
// }
// }
//
// self.drawCell(y: y, width: w, height: cellHeight, title: name, value: formatter.string(from: date), lineMargin: 0, context: ctx)
// y += cellHeight
//
// y += 16
// }
realHeight = y
}
return image.cutHeight(to: realHeight)
}
func reportText() -> String {
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .none
var text = (self.brand?.name?.original ?? "<unknown model>") + "\n"
text += "Year: \(self.year)\n"
if let color = self.color { text += "Color: \(color)\n" }
if let category = self.category { text += "Category: \(category)\n" }
var position = "Unknown"
if let rightWheel = self.isRightWheel {
position = rightWheel ? "Right" : "Left"
}
var japanese = "<Unknown>"
if let isJapanese = self.isJapanese {
japanese = isJapanese ? "Yes" : "No"
}
text += "Steering wheel position: \(position)\n"
text += "Japanese: \(japanese)\n"
text += "Plate number: \(self.getNumber())\n"
if let vin = self.vin1 { text += "VIN: \(vin)\n" }
if let sts = self.sts { text += "STS: \(sts)\n" }
if let pts = self.pts { text += "PTS: \(pts)\n" }
if let engineNumber = self.engine?.number { text += "Engine number: \(engineNumber)\n" }
if let fuel = self.engine?.fuelType { text += "Fuel type: \(fuel)\n" }
if let volume = self.engine?.volume { text += "Volume (cm³): \(volume)\n" }
if let powerHp = self.engine?.powerHp { text += "Power (HP): \(powerHp)\n" }
if let powerHp = self.engine?.powerHp { text += "Power (HP): \(powerHp)\n" }
if self.ownershipPeriods.count > 0 {
text += "\n"
text += "Ownership periods: \(self.ownershipPeriods.count)\n"
text += "\n"
for period in self.ownershipPeriods {
text += "Owner type: \(period.ownerType)\n"
let fromDate = Date(timeIntervalSince1970: TimeInterval(period.from/1000))
text += "From: \(formatter.string(from: fromDate))\n"
let toDate = Date(timeIntervalSince1970: TimeInterval(period.to/1000))
let toString = period.to == 0 ? "now" : formatter.string(from: toDate)
text += "To: \(toString)\n"
text += "Last operation: \(period.lastOperation)\n"
text += "\n"
}
}
if !self.events.isEmpty {
text += "\n"
text += "Locations\n"
text += "\n"
for event in self.events {
let date = Date(timeIntervalSince1970: event.date)
text += "Date: \(formatter.string(from: date))\n"
if let address = event.address {
text += "Address: \(address)\n"
}
let location = String(format: "%.05f, %.05f", event.latitude, event.longitude)
text += "Location: \(location)\n"
text += "\n"
}
}
return text
}
}

View File

@ -3,18 +3,17 @@ import os.log
import AVFoundation
import PKHUD
import AutoCatCore
import SwiftLocation
import CoreLocation
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@Service var settingsService: SettingsServiceProtocol
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 windowScene = (scene as? UIWindowScene) else { return }
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
if let activity = connectionOptions.userActivities.first {
@ -31,9 +30,43 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
}
Task {
try? await registerServices()
setupRootController(scene: scene)
}
}
func registerServices() async throws {
let container = ServiceContainer.shared
let settingsService = SettingsService(defaults: .standard)
container.register(SettingsServiceProtocol.self, instance: settingsService)
container.register(ApiServiceProtocol.self, instance: ApiService())
let locationService = LocationService(
geocoder: CLGeocoder(),
locationManager: Location(),
settingsService: settingsService
)
container.register(LocationServiceProtocol.self, instance: locationService)
container.register(StorageServiceProtocol.self,
instance: try await StorageService(settingsService: settingsService))
}
func setupRootController(scene: UIScene) {
guard let windowScene = (scene as? UIWindowScene) else {
return
}
self.window = UIWindow(windowScene: windowScene)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let settingsService = ServiceContainer.shared.resolve(SettingsServiceProtocol.self)
if settingsService.user.token.isEmpty {
self.window?.rootViewController = storyboard.instantiateViewController(identifier: "AuthController")
} else {

View File

@ -0,0 +1,34 @@
//
// HistoryCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 15.01.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import UIKit
import SwiftUI
import AutoCatCore
@MainActor
class HistoryCoordinator {
var navController: UINavigationController?
func start() -> UIViewController {
let resolver = ServiceContainer.shared
let viewModel = HistoryViewModel(
apiService: resolver.resolve(ApiServiceProtocol.self),
storageService: resolver.resolve(StorageServiceProtocol.self)
)
let view = HistoryScreen(viewModel: viewModel)
let controller = UIHostingController(rootView: view)
let navController = UINavigationController(rootViewController: controller)
self.navController = navController
return navController
}
}

View File

@ -0,0 +1,28 @@
//
// HistoryScreen.swift
// AutoCat
//
// Created by Selim Mustafaev on 15.01.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import SwiftUI
import AutoCatCore
struct HistoryScreen: View {
@State var viewModel: HistoryViewModel
var body: some View {
List(viewModel.vehicles) { vehicle in
LicensePlateView(number: PlateNumber(vehicle.getNumber()))
.frame(width: 200)
}
.listStyle(.plain)
.navigationTitle(String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""), viewModel.vehicles.count))
}
}
//#Preview {
// HistoryScreen(viewModel: .init())
//}

View File

@ -0,0 +1,33 @@
//
// HistoryViewModel.swift
// AutoCat
//
// Created by Selim Mustafaev on 15.01.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import SwiftUI
import AutoCatCore
@MainActor
@Observable
final class HistoryViewModel {
let apiService: ApiServiceProtocol
let storageService: StorageServiceProtocol
var vehicles: [VehicleDto] = []
init(apiService: ApiServiceProtocol, storageService: StorageServiceProtocol) {
self.apiService = apiService
self.storageService = storageService
Task { await loadVehicles() }
}
func loadVehicles() async {
vehicles = await storageService.loadVehicles()
}
}

View File

@ -0,0 +1,73 @@
//
// LicensePlateView.swift
// AutoCat
//
// Created by Selim Mustafaev on 15.01.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import SwiftUI
import AutoCatCore
struct LicensePlateView: View {
private static let aspectRatio: CGFloat = 520.0/112.0
private static let fontHeightCoeff: CGFloat = 58.0/76.0
private static let borderMultiplier = 0.01
private static let numberPartMultiplier = (1 - 3*LicensePlateView.borderMultiplier)*0.73
private static let reguinPartMultiplier = (1 - 3*LicensePlateView.borderMultiplier)*0.27
private static let outerCornerMultiplier = 0.03
private static let innerCornerMultiplier = outerCornerMultiplier - borderMultiplier
let number: PlateNumber
var body: some View {
GeometryReader { geometry in
ZStack {
RoundedRectangle(cornerRadius: geometry.size.width*LicensePlateView.outerCornerMultiplier)
.fill(Color("PlateForeground"))
HStack(spacing: 0) {
ZStack {
RoundedRectangle(cornerRadius: geometry.size.width*LicensePlateView.innerCornerMultiplier)
.fill(Color("PlateBackground"))
Text(number.mainPart())
.font(.custom("RoadNumbers", size: geometry.size.height))
.kerning(3)
.offset(y: geometry.size.width*0.015)
}
.frame(width: geometry.size.width*LicensePlateView.numberPartMultiplier)
.padding(geometry.size.width*LicensePlateView.borderMultiplier)
ZStack {
RoundedRectangle(cornerRadius: geometry.size.width*LicensePlateView.innerCornerMultiplier)
.fill(Color("PlateBackground"))
VStack(spacing: 0) {
Text(number.region())
.font(.custom("RoadNumbers", size: geometry.size.height*0.7))
HStack(spacing: 2) {
Text("RUS")
.font(.system(size: geometry.size.height*0.3))
VStack(spacing: 0) {
Rectangle().fill(.white)
Rectangle().fill(.blue)
Rectangle().fill(.red)
}
.aspectRatio(1.5, contentMode: .fit)
.frame(width: geometry.size.width*0.08)
}
}
.offset(y: geometry.size.width*0.015)
}
.frame(width: geometry.size.width*LicensePlateView.reguinPartMultiplier)
.padding(.vertical, geometry.size.width*LicensePlateView.borderMultiplier)
.padding(.trailing, geometry.size.width*LicensePlateView.borderMultiplier)
}
}
}
.aspectRatio(LicensePlateView.aspectRatio, contentMode: .fit)
}
}
#Preview {
LicensePlateView(number: PlateNumber("А123АА761"))
}

View File

@ -56,4 +56,9 @@ public actor StorageService: StorageServiceProtocol {
throw StorageError.vehicleNotFound
}
}
public func loadVehicles() async -> [VehicleDto] {
realm.objects(Vehicle.self).map(\.shallowDto)
}
}

View File

@ -11,11 +11,17 @@ import Mockable
@Mockable
public protocol StorageServiceProtocol: Sendable {
// Vehicles
func loadVehicles() async -> [VehicleDto]
func loadVehicle(number: String) async throws -> VehicleDto
func updateVehicleIfExists(dto: VehicleDto) async throws
// Notes
func addNote(text: String, to number: String) async throws -> VehicleDto
func deleteNote(id: String, for number: String) async throws -> VehicleDto
func editNote(id: String, text: String, for number: String) async throws -> VehicleDto
func updateVehicleIfExists(dto: VehicleDto) async throws
func loadVehicle(number: String) async throws -> VehicleDto
// Events
func add(event: VehicleEventDto, to number: String) async throws -> VehicleDto
func remove(event id: String, from number: String) async throws -> VehicleDto
func edit(event: VehicleEventDto, for number: String) async throws -> VehicleDto