From bade0d5418397998ea7ec36764e56585b4b81f00 Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Thu, 16 Jan 2025 23:23:25 +0300 Subject: [PATCH] Adding vehicle cell view. Displaying list of vehicles in sections. --- AutoCat.xcodeproj/project.pbxproj | 4 + .../Screens/HistoryScreen/HistoryScreen.swift | 14 +++- .../HistoryScreen/HistoryViewModel.swift | 18 ++++- AutoCat/SwiftUI/LicensePlateView.swift | 8 +- AutoCat/SwiftUI/VehicleCellView.swift | 74 +++++++++++++++++++ AutoCat/Utils/Formatters.swift | 7 ++ AutoCatCore/Models/DateSection.swift | 4 +- .../StorageService/StorageService.swift | 4 +- 8 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 AutoCat/SwiftUI/VehicleCellView.swift diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 65a1476..a775b17 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -146,6 +146,7 @@ 7AABBE3B2CF9F85600346588 /* Binding+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABBE3A2CF9F85600346588 /* Binding+Map.swift */; }; 7AABDE26253350C30041AFC6 /* RxSectionedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */; }; 7AB0EF812C5CC0FE00291EE6 /* SwiftLocationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */; }; + 7AB4E42C2D397D8E0006D052 /* VehicleCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4E42B2D397D8E0006D052 /* VehicleCellView.swift */; }; 7AB5871D2C42C1CF00FA7B66 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7AB5871C2C42C1CF00FA7B66 /* RealmSwift */; }; 7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */; }; 7AB587342C42D3FA00FA7B66 /* StorageService+Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587332C42D3FA00FA7B66 /* StorageService+Notes.swift */; }; @@ -417,6 +418,7 @@ 7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxSectionedDataSource.swift; sourceTree = ""; }; 7AAE6AD224CDDF950023860B /* VehicleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEvent.swift; sourceTree = ""; }; 7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftLocationProtocol.swift; sourceTree = ""; }; + 7AB4E42B2D397D8E0006D052 /* VehicleCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleCellView.swift; sourceTree = ""; }; 7AB562B9249C9E9B00473D53 /* VehicleRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRegion.swift; sourceTree = ""; }; 7AB587222C42D27F00FA7B66 /* AutoCatTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AutoCatTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageServiceProtocol.swift; sourceTree = ""; }; @@ -1082,6 +1084,7 @@ 7A4927D42CCE438600851C01 /* OptionalBinding.swift */, 7AABBE3A2CF9F85600346588 /* Binding+Map.swift */, 7A912F362D381B7400002938 /* LicensePlateView.swift */, + 7AB4E42B2D397D8E0006D052 /* VehicleCellView.swift */, ); path = SwiftUI; sourceTree = ""; @@ -1345,6 +1348,7 @@ 7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */, 7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */, 7A06E0AE2C7065C7005731AC /* SettingsViewModel.swift in Sources */, + 7AB4E42C2D397D8E0006D052 /* VehicleCellView.swift in Sources */, 7A961C6E2C4C3C9E00CE2211 /* LinkRowView.swift in Sources */, 7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */, 7A3E30F32C18840600567704 /* ActivityItemSource.swift in Sources */, diff --git a/AutoCat/Screens/HistoryScreen/HistoryScreen.swift b/AutoCat/Screens/HistoryScreen/HistoryScreen.swift index 45a3cda..d33464e 100644 --- a/AutoCat/Screens/HistoryScreen/HistoryScreen.swift +++ b/AutoCat/Screens/HistoryScreen/HistoryScreen.swift @@ -14,12 +14,18 @@ struct HistoryScreen: View { @State var viewModel: HistoryViewModel var body: some View { - List(viewModel.vehicles) { vehicle in - LicensePlateView(number: PlateNumber(vehicle.getNumber())) - .frame(width: 200) + List { + ForEach(viewModel.vehicleSections) { section in + Section(header: Text(section.header)) { + ForEach(section.elements) { vehicle in + VehicleCellView(vehicle: vehicle) + } + } + } } .listStyle(.plain) - .navigationTitle(String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""), viewModel.vehicles.count)) + .navigationTitle(String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""), viewModel.vehicleSections.reduce(0, { $0 + $1.elements.count }))) + .searchable(text: $viewModel.searchText, prompt: "Search plate numbers") } } diff --git a/AutoCat/Screens/HistoryScreen/HistoryViewModel.swift b/AutoCat/Screens/HistoryScreen/HistoryViewModel.swift index 5c3c932..f97bee3 100644 --- a/AutoCat/Screens/HistoryScreen/HistoryViewModel.swift +++ b/AutoCat/Screens/HistoryScreen/HistoryViewModel.swift @@ -16,7 +16,16 @@ final class HistoryViewModel { let apiService: ApiServiceProtocol let storageService: StorageServiceProtocol - var vehicles: [VehicleDto] = [] + var vehicleSections: [DateSection] = [] + var vehicleSectionsFiltered: [DateSection] = [] + + var searchText: String = "" { + didSet { + if searchText != oldValue { + applySearchFilter(text: searchText) + } + } + } init(apiService: ApiServiceProtocol, storageService: StorageServiceProtocol) { @@ -28,6 +37,11 @@ final class HistoryViewModel { func loadVehicles() async { - vehicles = await storageService.loadVehicles() + let vehicles = await storageService.loadVehicles() + vehicleSections = vehicles.groupedByDate(type: .updatedDate) + } + + func applySearchFilter(text: String) { + } } diff --git a/AutoCat/SwiftUI/LicensePlateView.swift b/AutoCat/SwiftUI/LicensePlateView.swift index 5908817..34b3436 100644 --- a/AutoCat/SwiftUI/LicensePlateView.swift +++ b/AutoCat/SwiftUI/LicensePlateView.swift @@ -20,17 +20,19 @@ struct LicensePlateView: View { private static let innerCornerMultiplier = outerCornerMultiplier - borderMultiplier let number: PlateNumber + let foreground: Color var body: some View { GeometryReader { geometry in ZStack { RoundedRectangle(cornerRadius: geometry.size.width*LicensePlateView.outerCornerMultiplier) - .fill(Color("PlateForeground")) + .fill(foreground) HStack(spacing: 0) { ZStack { RoundedRectangle(cornerRadius: geometry.size.width*LicensePlateView.innerCornerMultiplier) .fill(Color("PlateBackground")) Text(number.mainPart()) + .foregroundStyle(foreground) .font(.custom("RoadNumbers", size: geometry.size.height)) .kerning(3) .offset(y: geometry.size.width*0.015) @@ -43,9 +45,11 @@ struct LicensePlateView: View { .fill(Color("PlateBackground")) VStack(spacing: 0) { Text(number.region()) + .foregroundStyle(foreground) .font(.custom("RoadNumbers", size: geometry.size.height*0.7)) HStack(spacing: 2) { Text("RUS") + .foregroundStyle(foreground) .font(.system(size: geometry.size.height*0.3)) VStack(spacing: 0) { Rectangle().fill(.white) @@ -69,5 +73,5 @@ struct LicensePlateView: View { } #Preview { - LicensePlateView(number: PlateNumber("А123АА761")) + LicensePlateView(number: PlateNumber("А123АА761"), foreground: Color("PlateForeground")) } diff --git a/AutoCat/SwiftUI/VehicleCellView.swift b/AutoCat/SwiftUI/VehicleCellView.swift new file mode 100644 index 0000000..14cd700 --- /dev/null +++ b/AutoCat/SwiftUI/VehicleCellView.swift @@ -0,0 +1,74 @@ +// +// VehicleCellView.swift +// AutoCat +// +// Created by Selim Mustafaev on 16.01.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import SwiftUI +import AutoCatCore + +struct VehicleCellView: View { + + let vehicle: VehicleDto + + var body: some View { + VStack { + HStack(spacing: 4) { + if let name = vehicle.brand?.name?.original { + Text(name) + .font(.headline) + } + + Spacer(minLength: 0) + + if vehicle.synchronized || vehicle.unrecognized { + Image(systemName: "exclamationmark.arrow.triangle") + .frame(width: 20, height: 20) + .foregroundStyle(.orange) + } + + if !vehicle.notes.isEmpty { + Image(systemName: "text.bubble") + .frame(width: 20, height: 20) + .foregroundStyle(.blue) + Text(String(vehicle.notes.count)) + .font(.callout) + } + } + + HStack(alignment: .bottom, spacing: 8) { + LicensePlateView(number: PlateNumber(vehicle.getNumber()), foreground: getForegroundColor()) + .frame(height: 40) + + Spacer(minLength: 8) + + VStack(alignment: .trailing, spacing: 8) { + if vehicle.updatedDate != vehicle.addedDate { + Text(Formatters.short.string(from: Date(timeIntervalSince1970: vehicle.updatedDate))) + .font(.footnote) + .foregroundStyle(.secondary) + } + Text(Formatters.short.string(from: Date(timeIntervalSince1970: vehicle.addedDate))) + .font(.footnote) + .foregroundStyle(.tertiary) + } + } + } + } + + func getForegroundColor() -> Color { + if vehicle.unrecognized { + Color(UIColor.systemRed) + } else if vehicle.outdated { + Color(UIColor.systemGray3) + } else { + Color("PlateForeground") + } + } +} + +#Preview { + VehicleCell() +} diff --git a/AutoCat/Utils/Formatters.swift b/AutoCat/Utils/Formatters.swift index f353c6e..b3d03bc 100644 --- a/AutoCat/Utils/Formatters.swift +++ b/AutoCat/Utils/Formatters.swift @@ -23,4 +23,11 @@ struct Formatters { formatter.timeStyle = .short return formatter }() + + static let short: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .short + return formatter + }() } diff --git a/AutoCatCore/Models/DateSection.swift b/AutoCatCore/Models/DateSection.swift index fb8e166..7b4e579 100644 --- a/AutoCatCore/Models/DateSection.swift +++ b/AutoCatCore/Models/DateSection.swift @@ -1,8 +1,9 @@ import Foundation import SwiftDate -public struct DateSection { +public struct DateSection: Identifiable { + public var id = UUID() var timestamp: Double = 0 var dateString: String public var elements: [T] @@ -20,6 +21,7 @@ public struct DateSection { public init(original: DateSection, items: [T]) { self = original self.elements = items + self.id = original.id } public init(timestamp: Double, items: [T], monthStart: DateInRegion, weekStart: DateInRegion) { diff --git a/AutoCatCore/Services/StorageService/StorageService.swift b/AutoCatCore/Services/StorageService/StorageService.swift index 48af223..f7f9345 100644 --- a/AutoCatCore/Services/StorageService/StorageService.swift +++ b/AutoCatCore/Services/StorageService/StorageService.swift @@ -59,6 +59,8 @@ public actor StorageService: StorageServiceProtocol { public func loadVehicles() async -> [VehicleDto] { - realm.objects(Vehicle.self).map(\.shallowDto) + realm.objects(Vehicle.self) + .sorted(byKeyPath: "updatedDate", ascending: false) + .map(\.shallowDto) } }