Adding sharing to SwiftUI version of history screen

This commit is contained in:
Selim Mustafaev 2025-01-19 15:39:56 +03:00
parent 13a0bbe895
commit a7f4e6b3c5
8 changed files with 123 additions and 10 deletions

View File

@ -48,7 +48,6 @@
7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */; };
7A1E78FF2CE91A740004B740 /* Vehicle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78FE2CE91A740004B740 /* Vehicle.swift */; };
7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF2249F8B650035F39E /* RecordsController.swift */; };
7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */; };
7A27ADF7249FEF690035F39E /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF6249FEF690035F39E /* Recorder.swift */; };
7A2C96122C3B155B00AE46B5 /* NoteAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2C96112C3B155B00AE46B5 /* NoteAlertModifier.swift */; };
7A2E11292CCE395300E5CA17 /* OptionalDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E11282CCE395300E5CA17 /* OptionalDatePicker.swift */; };
@ -147,6 +146,8 @@
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 */; };
7AB4E4332D3C21C00006D052 /* FileManagerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4E4322D3C21C00006D052 /* FileManagerExt.swift */; };
7AB4E4382D3D0C5C0006D052 /* VehiclesArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4E4372D3D0C5C0006D052 /* VehiclesArchive.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 */; };
@ -309,7 +310,6 @@
7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportCoordinator.swift; sourceTree = "<group>"; };
7A1E78FE2CE91A740004B740 /* Vehicle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = "<group>"; };
7A27ADF2249F8B650035F39E /* RecordsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsController.swift; sourceTree = "<group>"; };
7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExt.swift; sourceTree = "<group>"; };
7A27ADF6249FEF690035F39E /* Recorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recorder.swift; sourceTree = "<group>"; };
7A27ADF824A09CAD0035F39E /* CocoaError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaError.swift; sourceTree = "<group>"; };
7A2C96112C3B155B00AE46B5 /* NoteAlertModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteAlertModifier.swift; sourceTree = "<group>"; };
@ -419,6 +419,8 @@
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>"; };
7AB4E4322D3C21C00006D052 /* FileManagerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExt.swift; sourceTree = "<group>"; };
7AB4E4372D3D0C5C0006D052 /* VehiclesArchive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclesArchive.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>"; };
@ -659,6 +661,7 @@
7A11474823FF2B2D00B424AF /* Response.swift */,
7A11474623FF2AA500B424AF /* User.swift */,
7AB562B9249C9E9B00473D53 /* VehicleRegion.swift */,
7AB4E4372D3D0C5C0006D052 /* VehiclesArchive.swift */,
);
path = Models;
sourceTree = "<group>";
@ -738,7 +741,6 @@
7A8AB76425A0DB8F00ECF2C1 /* BundleVersion.swift */,
7AE24C5E251F1B4E00758E39 /* Buttons.swift */,
7A3F07AA24360DC800E59687 /* Dated.swift */,
7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */,
7ADF6CA02512244400F237B2 /* MapExt.swift */,
7ADF6C92250B954900F237B2 /* Navigation.swift */,
7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */,
@ -1061,6 +1063,7 @@
7AF6D2292677C3950086EA64 /* Extensions */ = {
isa = PBXGroup;
children = (
7AB4E4322D3C21C00006D052 /* FileManagerExt.swift */,
7A27ADF824A09CAD0035F39E /* CocoaError.swift */,
7AE8424D26109F78002F6B31 /* Exportable.swift */,
7A1CF81529A42117007962DA /* Realm.swift */,
@ -1342,7 +1345,6 @@
7AF860702CBAA24500954D2F /* NavigationLink.swift in Sources */,
7AB9FE262D08C2D7005DE374 /* EventsCoordinator.swift in Sources */,
7AB9FE282D08C2F4005DE374 /* EventsViewModel.swift in Sources */,
7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */,
7A4927D52CCE438600851C01 /* OptionalBinding.swift in Sources */,
7A17CE4A2A2E820300626A6E /* UIStackView.swift in Sources */,
7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */,
@ -1485,6 +1487,7 @@
7A60D24F2C5A9DA800D13F7B /* LocationServiceProtocol.swift in Sources */,
7A761C07267E8E7F0005F28F /* AnyEncodable.swift in Sources */,
7A64A2032C19DA1000284124 /* VehicleDto.swift in Sources */,
7AB4E4332D3C21C00006D052 /* FileManagerExt.swift in Sources */,
7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */,
7A3E12D72C7B42B700EE710D /* UserDefaults+Settings.swift in Sources */,
7AA514E02D0B75B3001CAC50 /* StorageService+Events.swift in Sources */,
@ -1493,6 +1496,7 @@
7A64A2202C19E93500284124 /* VehicleNoteDto.swift in Sources */,
7AF6D21A2677C1680086EA64 /* User.swift in Sources */,
7A60D2512C5A9E4200D13F7B /* GeocoderProtocol.swift in Sources */,
7AB4E4382D3D0C5C0006D052 /* VehiclesArchive.swift in Sources */,
7A64A21C2C19E87B00284124 /* OsagoDto.swift in Sources */,
7AF6D21D2677C1680086EA64 /* Osago.swift in Sources */,
7A1CF81629A42117007962DA /* Realm.swift in Sources */,

View File

@ -13,6 +13,7 @@ struct HistoryScreen: View {
@State var viewModel: HistoryViewModel
@State var filterSheetPresented = false
@State var exportSheetPresented = false
var body: some View {
List {
@ -24,14 +25,20 @@ struct HistoryScreen: View {
}
}
}
.hud($viewModel.hud)
.listStyle(.plain)
.navigationTitle(String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""),
viewModel.vehicleSections.reduce(0, { $0 + $1.elements.count })))
viewModel.vehiclesCount))
.searchable(text: $viewModel.searchText, prompt: "Search plate numbers")
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.keyboardType(.asciiCapable)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("", systemImage: "square.and.arrow.up") {
exportSheetPresented = true
}
}
ToolbarItem(placement: .primaryAction) {
Button("", systemImage: "line.horizontal.3.decrease") {
filterSheetPresented = true
@ -46,6 +53,18 @@ struct HistoryScreen: View {
}
}
}
.confirmationDialog("Export history as", isPresented: $exportSheetPresented, titleVisibility: .visible) {
ShareLink(item: viewModel.vehiclesArchive, preview: SharePreview(VehiclesArchive.fileName)) {
Text("CSV table")
}
if let dbUrl = viewModel.dbFileURL {
ShareLink(item: dbUrl) {
Text("Database file")
}
}
}
}
}

View File

@ -11,11 +11,13 @@ import AutoCatCore
@MainActor
@Observable
final class HistoryViewModel {
final class HistoryViewModel: ACHudContainer {
let apiService: ApiServiceProtocol
let storageService: StorageServiceProtocol
var hud: ACHud?
var vehicles: [VehicleDto] = []
var vehiclesFiltered: [VehicleDto] = []
var vehicleSections: [DateSection<VehicleDto>] = []
@ -30,12 +32,24 @@ final class HistoryViewModel {
var filter: HistoryFilter = .all
init(apiService: ApiServiceProtocol, storageService: StorageServiceProtocol) {
var vehiclesArchive: VehiclesArchive {
VehiclesArchive(vehiles: vehiclesFiltered)
}
var vehiclesCount: Int {
vehicleSections.reduce(0, { $0 + $1.elements.count })
}
var dbFileURL: URL?
init(apiService: ApiServiceProtocol,
storageService: StorageServiceProtocol) {
self.apiService = apiService
self.storageService = storageService
Task { await loadVehicles() }
Task { dbFileURL = await storageService.dbFileURL }
}
func loadVehicles() async {

View File

@ -23,8 +23,8 @@ struct VehicleCellView: View {
Spacer(minLength: 0)
if vehicle.synchronized || vehicle.unrecognized {
Image(systemName: "exclamationmark.arrow.triangle")
if !vehicle.synchronized && !vehicle.unrecognized {
Image(systemName: "exclamationmark.arrow.triangle.2.circlepath")
.frame(width: 20, height: 20)
.foregroundStyle(.orange)
}

View File

@ -1,6 +1,6 @@
import Foundation
extension FileManager {
public extension FileManager {
func url(for file: String, in dir: String) throws -> URL {
guard let docUrl = self.urls(for: .documentDirectory, in: .userDomainMask).first else {
throw CocoaError(.fileReadNoSuchFile)

View File

@ -0,0 +1,66 @@
//
// VehiclesArchive.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 19.01.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import SwiftUI
public enum VehiclesArchiveError: LocalizedError {
case filedCreateCsv
public var errorDescription: String? {
switch self {
case .filedCreateCsv: "Failed to create csv data for vehicles"
}
}
}
public final class VehiclesArchive {
let vehicles: [VehicleDto]
public init(vehiles: [VehicleDto]) {
self.vehicles = vehiles
}
func makeCsvString() throws -> String {
var result = ""
let newLine: Character = "\r\n"
result.append(VehicleDto.csvHeader)
result.append(newLine)
for vehicle in vehicles {
result.append(vehicle.csvLine)
result.append(newLine)
}
return result
}
}
extension VehiclesArchive: Transferable {
public static var fileName: String {
"autocat.csv"
}
public static var transferRepresentation: some TransferRepresentation {
DataRepresentation(exportedContentType: .commaSeparatedText) { archive in
let csvString = try archive.makeCsvString()
if let data = csvString.data(using: .utf8){
return data
} else {
throw VehiclesArchiveError.filedCreateCsv
}
}
.suggestedFileName(fileName)
}
}

View File

@ -37,6 +37,12 @@ public actor StorageService: StorageServiceProtocol {
realm = try await Realm(configuration: config, actor: self)
}
public var dbFileURL: URL? {
get async {
realm.configuration.fileURL
}
}
public func updateVehicleIfExists(dto: VehicleDto) async throws {
guard realm.object(ofType: Vehicle.self, forPrimaryKey: dto.getNumber()) != nil else {

View File

@ -7,10 +7,14 @@
//
import Mockable
import Foundation
@Mockable
public protocol StorageServiceProtocol: Sendable {
// Generic
var dbFileURL: URL? { get async }
// Vehicles
func loadVehicles() async -> [VehicleDto]
func loadVehicle(number: String) async throws -> VehicleDto