Adding vehicle cell view. Displaying list of vehicles in sections.
This commit is contained in:
parent
67977f1ebf
commit
bade0d5418
@ -146,6 +146,7 @@
|
|||||||
7AABBE3B2CF9F85600346588 /* Binding+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABBE3A2CF9F85600346588 /* Binding+Map.swift */; };
|
7AABBE3B2CF9F85600346588 /* Binding+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABBE3A2CF9F85600346588 /* Binding+Map.swift */; };
|
||||||
7AABDE26253350C30041AFC6 /* RxSectionedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */; };
|
7AABDE26253350C30041AFC6 /* RxSectionedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */; };
|
||||||
7AB0EF812C5CC0FE00291EE6 /* SwiftLocationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.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 */; };
|
7AB5871D2C42C1CF00FA7B66 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7AB5871C2C42C1CF00FA7B66 /* RealmSwift */; };
|
||||||
7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */; };
|
7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */; };
|
||||||
7AB587342C42D3FA00FA7B66 /* StorageService+Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587332C42D3FA00FA7B66 /* StorageService+Notes.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageServiceProtocol.swift; sourceTree = "<group>"; };
|
||||||
@ -1082,6 +1084,7 @@
|
|||||||
7A4927D42CCE438600851C01 /* OptionalBinding.swift */,
|
7A4927D42CCE438600851C01 /* OptionalBinding.swift */,
|
||||||
7AABBE3A2CF9F85600346588 /* Binding+Map.swift */,
|
7AABBE3A2CF9F85600346588 /* Binding+Map.swift */,
|
||||||
7A912F362D381B7400002938 /* LicensePlateView.swift */,
|
7A912F362D381B7400002938 /* LicensePlateView.swift */,
|
||||||
|
7AB4E42B2D397D8E0006D052 /* VehicleCellView.swift */,
|
||||||
);
|
);
|
||||||
path = SwiftUI;
|
path = SwiftUI;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1345,6 +1348,7 @@
|
|||||||
7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */,
|
7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */,
|
||||||
7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */,
|
7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */,
|
||||||
7A06E0AE2C7065C7005731AC /* SettingsViewModel.swift in Sources */,
|
7A06E0AE2C7065C7005731AC /* SettingsViewModel.swift in Sources */,
|
||||||
|
7AB4E42C2D397D8E0006D052 /* VehicleCellView.swift in Sources */,
|
||||||
7A961C6E2C4C3C9E00CE2211 /* LinkRowView.swift in Sources */,
|
7A961C6E2C4C3C9E00CE2211 /* LinkRowView.swift in Sources */,
|
||||||
7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */,
|
7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */,
|
||||||
7A3E30F32C18840600567704 /* ActivityItemSource.swift in Sources */,
|
7A3E30F32C18840600567704 /* ActivityItemSource.swift in Sources */,
|
||||||
|
|||||||
@ -14,12 +14,18 @@ struct HistoryScreen: View {
|
|||||||
@State var viewModel: HistoryViewModel
|
@State var viewModel: HistoryViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List(viewModel.vehicles) { vehicle in
|
List {
|
||||||
LicensePlateView(number: PlateNumber(vehicle.getNumber()))
|
ForEach(viewModel.vehicleSections) { section in
|
||||||
.frame(width: 200)
|
Section(header: Text(section.header)) {
|
||||||
|
ForEach(section.elements) { vehicle in
|
||||||
|
VehicleCellView(vehicle: vehicle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,16 @@ final class HistoryViewModel {
|
|||||||
let apiService: ApiServiceProtocol
|
let apiService: ApiServiceProtocol
|
||||||
let storageService: StorageServiceProtocol
|
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) {
|
init(apiService: ApiServiceProtocol, storageService: StorageServiceProtocol) {
|
||||||
|
|
||||||
@ -28,6 +37,11 @@ final class HistoryViewModel {
|
|||||||
|
|
||||||
func loadVehicles() async {
|
func loadVehicles() async {
|
||||||
|
|
||||||
vehicles = await storageService.loadVehicles()
|
let vehicles = await storageService.loadVehicles()
|
||||||
|
vehicleSections = vehicles.groupedByDate(type: .updatedDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applySearchFilter(text: String) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,17 +20,19 @@ struct LicensePlateView: View {
|
|||||||
private static let innerCornerMultiplier = outerCornerMultiplier - borderMultiplier
|
private static let innerCornerMultiplier = outerCornerMultiplier - borderMultiplier
|
||||||
|
|
||||||
let number: PlateNumber
|
let number: PlateNumber
|
||||||
|
let foreground: Color
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: geometry.size.width*LicensePlateView.outerCornerMultiplier)
|
RoundedRectangle(cornerRadius: geometry.size.width*LicensePlateView.outerCornerMultiplier)
|
||||||
.fill(Color("PlateForeground"))
|
.fill(foreground)
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
ZStack {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: geometry.size.width*LicensePlateView.innerCornerMultiplier)
|
RoundedRectangle(cornerRadius: geometry.size.width*LicensePlateView.innerCornerMultiplier)
|
||||||
.fill(Color("PlateBackground"))
|
.fill(Color("PlateBackground"))
|
||||||
Text(number.mainPart())
|
Text(number.mainPart())
|
||||||
|
.foregroundStyle(foreground)
|
||||||
.font(.custom("RoadNumbers", size: geometry.size.height))
|
.font(.custom("RoadNumbers", size: geometry.size.height))
|
||||||
.kerning(3)
|
.kerning(3)
|
||||||
.offset(y: geometry.size.width*0.015)
|
.offset(y: geometry.size.width*0.015)
|
||||||
@ -43,9 +45,11 @@ struct LicensePlateView: View {
|
|||||||
.fill(Color("PlateBackground"))
|
.fill(Color("PlateBackground"))
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Text(number.region())
|
Text(number.region())
|
||||||
|
.foregroundStyle(foreground)
|
||||||
.font(.custom("RoadNumbers", size: geometry.size.height*0.7))
|
.font(.custom("RoadNumbers", size: geometry.size.height*0.7))
|
||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
Text("RUS")
|
Text("RUS")
|
||||||
|
.foregroundStyle(foreground)
|
||||||
.font(.system(size: geometry.size.height*0.3))
|
.font(.system(size: geometry.size.height*0.3))
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Rectangle().fill(.white)
|
Rectangle().fill(.white)
|
||||||
@ -69,5 +73,5 @@ struct LicensePlateView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
LicensePlateView(number: PlateNumber("А123АА761"))
|
LicensePlateView(number: PlateNumber("А123АА761"), foreground: Color("PlateForeground"))
|
||||||
}
|
}
|
||||||
|
|||||||
74
AutoCat/SwiftUI/VehicleCellView.swift
Normal file
74
AutoCat/SwiftUI/VehicleCellView.swift
Normal 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()
|
||||||
|
}
|
||||||
@ -23,4 +23,11 @@ struct Formatters {
|
|||||||
formatter.timeStyle = .short
|
formatter.timeStyle = .short
|
||||||
return formatter
|
return formatter
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
static let short: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .short
|
||||||
|
formatter.timeStyle = .short
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftDate
|
import SwiftDate
|
||||||
|
|
||||||
public struct DateSection<T: Identifiable> {
|
public struct DateSection<T: Identifiable>: Identifiable {
|
||||||
|
|
||||||
|
public var id = UUID()
|
||||||
var timestamp: Double = 0
|
var timestamp: Double = 0
|
||||||
var dateString: String
|
var dateString: String
|
||||||
public var elements: [T]
|
public var elements: [T]
|
||||||
@ -20,6 +21,7 @@ public struct DateSection<T: Identifiable> {
|
|||||||
public init(original: DateSection, items: [T]) {
|
public init(original: DateSection, items: [T]) {
|
||||||
self = original
|
self = original
|
||||||
self.elements = items
|
self.elements = items
|
||||||
|
self.id = original.id
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(timestamp: Double, items: [T], monthStart: DateInRegion, weekStart: DateInRegion) {
|
public init(timestamp: Double, items: [T], monthStart: DateInRegion, weekStart: DateInRegion) {
|
||||||
|
|||||||
@ -59,6 +59,8 @@ public actor StorageService: StorageServiceProtocol {
|
|||||||
|
|
||||||
public func loadVehicles() async -> [VehicleDto] {
|
public func loadVehicles() async -> [VehicleDto] {
|
||||||
|
|
||||||
realm.objects(Vehicle.self).map(\.shallowDto)
|
realm.objects(Vehicle.self)
|
||||||
|
.sorted(byKeyPath: "updatedDate", ascending: false)
|
||||||
|
.map(\.shallowDto)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user