Adding vehicle cell view. Displaying list of vehicles in sections.

This commit is contained in:
Selim Mustafaev 2025-01-16 23:23:25 +03:00
parent 67977f1ebf
commit bade0d5418
8 changed files with 123 additions and 10 deletions

View File

@ -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 = "<group>"; };
7AAE6AD224CDDF950023860B /* VehicleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEvent.swift; sourceTree = "<group>"; };
7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftLocationProtocol.swift; sourceTree = "<group>"; };
7AB4E42B2D397D8E0006D052 /* VehicleCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleCellView.swift; sourceTree = "<group>"; };
7AB562B9249C9E9B00473D53 /* VehicleRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRegion.swift; sourceTree = "<group>"; };
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 = "<group>"; };
@ -1082,6 +1084,7 @@
7A4927D42CCE438600851C01 /* OptionalBinding.swift */,
7AABBE3A2CF9F85600346588 /* Binding+Map.swift */,
7A912F362D381B7400002938 /* LicensePlateView.swift */,
7AB4E42B2D397D8E0006D052 /* VehicleCellView.swift */,
);
path = SwiftUI;
sourceTree = "<group>";
@ -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 */,

View File

@ -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")
}
}

View File

@ -16,7 +16,16 @@ final class HistoryViewModel {
let apiService: ApiServiceProtocol
let storageService: StorageServiceProtocol
var vehicles: [VehicleDto] = []
var vehicleSections: [DateSection<VehicleDto>] = []
var vehicleSectionsFiltered: [DateSection<VehicleDto>] = []
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) {
}
}

View File

@ -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"))
}

View File

@ -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()
}

View File

@ -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
}()
}

View File

@ -1,8 +1,9 @@
import Foundation
import SwiftDate
public struct DateSection<T: Identifiable> {
public struct DateSection<T: Identifiable>: Identifiable {
public var id = UUID()
var timestamp: Double = 0
var dateString: String
public var elements: [T]
@ -20,6 +21,7 @@ public struct DateSection<T: Identifiable> {
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) {

View File

@ -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)
}
}