Adding navigation from new report to all detail screens. Adding sharing report link.

This commit is contained in:
Selim Mustafaev 2024-12-07 22:42:11 +03:00
parent 97e35ac785
commit 37b94aca62
10 changed files with 250 additions and 27 deletions

View File

@ -157,6 +157,9 @@
7AB587412C42FFE200FA7B66 /* ApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587402C42FFE200FA7B66 /* ApiServiceProtocol.swift */; };
7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8B2435C38700258F61 /* CustomTextField.swift */; };
7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8D2435D1A000258F61 /* CustomButton.swift */; };
7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B462D044A3200B43213 /* GalleryScreen.swift */; };
7ABD1B492D044A4700B43213 /* GalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */; };
7ABD1B4B2D044A7D00B43213 /* GalleryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */; };
7AC3554A2969652F00889457 /* SwiftEntryKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7AC355492969652F00889457 /* SwiftEntryKit */; };
7AC3554C29696A1C00889457 /* MainTabController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3554B29696A1C00889457 /* MainTabController.swift */; };
7AC3554E29696C4500889457 /* DummyNewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3554D29696C4500889457 /* DummyNewController.swift */; };
@ -428,6 +431,9 @@
7AB587402C42FFE200FA7B66 /* ApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceProtocol.swift; sourceTree = "<group>"; };
7AB67E8B2435C38700258F61 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; };
7AB67E8D2435D1A000258F61 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = "<group>"; };
7ABD1B462D044A3200B43213 /* GalleryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryScreen.swift; sourceTree = "<group>"; };
7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewModel.swift; sourceTree = "<group>"; };
7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryCoordinator.swift; sourceTree = "<group>"; };
7AC3554B29696A1C00889457 /* MainTabController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabController.swift; sourceTree = "<group>"; };
7AC3554D29696C4500889457 /* DummyNewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyNewController.swift; sourceTree = "<group>"; };
7AC3554F29696D5A00889457 /* NewNumberController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNumberController.swift; sourceTree = "<group>"; };
@ -688,6 +694,7 @@
7A1441632C297E9800E79018 /* Screens */ = {
isa = PBXGroup;
children = (
7ABD1B452D044A0900B43213 /* GalleryScreen */,
7A1E78F42CE9001A0004B740 /* ReportScreen */,
7A43228F2CB2CC5D00085CF6 /* FiltersScreen */,
7A06E0AA2C706550005731AC /* SettingsScreen */,
@ -989,6 +996,16 @@
path = ApiService;
sourceTree = "<group>";
};
7ABD1B452D044A0900B43213 /* GalleryScreen */ = {
isa = PBXGroup;
children = (
7ABD1B462D044A3200B43213 /* GalleryScreen.swift */,
7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */,
7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */,
);
path = GalleryScreen;
sourceTree = "<group>";
};
7AC355552969742800889457 /* ACUIKit */ = {
isa = PBXGroup;
children = (
@ -1356,6 +1373,7 @@
7A1E78F62CE900330004B740 /* ReportScreen.swift in Sources */,
7A10226C2C551EC500B84627 /* LocationEditScreen.swift in Sources */,
7A7158072C44085600852088 /* OsagoScreen.swift in Sources */,
7ABD1B492D044A4700B43213 /* GalleryViewModel.swift in Sources */,
7AAAFAD32C4D0FD00050410D /* ACImageSliderView.swift in Sources */,
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */,
7A33381124990DAE00D878F1 /* FiltersController.swift in Sources */,
@ -1399,6 +1417,7 @@
7A7158002C43EA6900852088 /* OwnersScreen.swift in Sources */,
7A1441702C2998B200E79018 /* Formatters.swift in Sources */,
7A4322912CB2CC8A00085CF6 /* FiltersScreen.swift in Sources */,
7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */,
7ADF6C95250D037700F237B2 /* ShowEventController.swift in Sources */,
7A71580C2C44453200852088 /* AdsScreen.swift in Sources */,
7A06E0B02C7065D8005731AC /* SettingsCoordinator.swift in Sources */,
@ -1407,6 +1426,7 @@
7AFBE8CC2C3085C6003C491D /* ACProgressView.swift in Sources */,
7ADF6C93250B954900F237B2 /* Navigation.swift in Sources */,
7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */,
7ABD1B4B2D044A7D00B43213 /* GalleryCoordinator.swift in Sources */,
7A64AE732469DFB600ABE48E /* DismissAnimationController.swift in Sources */,
7ADF6C97250F41B000F237B2 /* PNKeyboard.swift in Sources */,
7A1022702C551EFD00B84627 /* LocationEditCoordinator.swift in Sources */,

View File

@ -0,0 +1,31 @@
//
// GalleryCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 07.12.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import UIKit
import SwiftUI
import AutoCatCore
@MainActor
class GalleryCoordinator: Coordinator {
let viewController: UINavigationController
let photos: [VehiclePhotoDto]
init(navController: UINavigationController, photos: [VehiclePhotoDto]) {
self.viewController = navController
self.photos = photos
}
func start() async throws {
let viewModel = GalleryViewModel(photos: photos)
let controller = UIHostingController(rootView: GalleryScreen(viewModel: viewModel))
viewController.pushViewController(controller, animated: true)
}
}

View File

@ -0,0 +1,72 @@
//
// GalleryScreen.swift
// AutoCat
//
// Created by Selim Mustafaev on 07.12.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import SwiftUI
import AutoCatCore
struct GalleryScreen: View {
@State var viewModel: GalleryViewModel
@State var galleryModel: ACImageSliderModel?
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 2) {
ForEach(viewModel.photos) { photo in
ZStack {
AsyncImage(url: URL(string: photo.url)) { phase in
switch phase {
case .success(let image):
image
.resizable()
.scaledToFill()
default:
ZStack {
Rectangle()
.foregroundStyle(.quaternary)
Image(systemName: "photo")
.font(.system(size: 48))
.foregroundStyle(.tertiary)
}
}
}
.layoutPriority(-1)
.onTapGesture {
guard let url = URL(string: photo.url) else {
return
}
galleryModel = ACImageSliderModel(
urls: viewModel.photos.compactMap { URL(string: $0.url) },
selected: url
)
}
Color.clear
}
.aspectRatio(1, contentMode: .fit)
.clipped()
}
}
}
.imageSlider($galleryModel)
}
var columns: [GridItem] {
[
GridItem(.flexible(), spacing: 2),
GridItem(.flexible(), spacing: 2),
GridItem(.flexible(), spacing: 2)
]
}
}
#Preview {
GalleryScreen(viewModel: .init(photos: []))
}

View File

@ -0,0 +1,21 @@
//
// GalleryViewModel.swift
// AutoCat
//
// Created by Selim Mustafaev on 07.12.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import SwiftUI
import AutoCatCore
@MainActor
@Observable
class GalleryViewModel {
let photos: [VehiclePhotoDto]
init(photos: [VehiclePhotoDto]) {
self.photos = photos
}
}

View File

@ -22,10 +22,12 @@ class NotesCoordinator: Coordinator {
self.vehicle = vehicle
}
func start() async throws {
func start() async throws -> VehicleDto {
let viewModel = NotesViewModel(vehicle: vehicle)
let controller = UIHostingController(rootView: NotesScreen(viewModel: viewModel))
let controller = CustomHostingController(rootView: NotesScreen(viewModel: viewModel))
viewController?.pushViewController(controller, animated: true)
await controller.waitForDisappear()
return viewModel.vehicle
}
}

View File

@ -17,7 +17,7 @@ class ReportCoordinator: Coordinator {
let vehicle: VehicleDto
let isPersistent: Bool
var navController: UINavigationController?
weak var navController: UINavigationController?
init(splitController: UISplitViewController?, vehicle: VehicleDto, isPersistent: Bool) {
@ -34,26 +34,26 @@ class ReportCoordinator: Coordinator {
let viewModel = ReportViewModel(vehicle: vehicle, isPersistent: isPersistent)
viewModel.coordinator = self
let controller = UIHostingController(rootView: ReportScreen(viewModel: viewModel))
navController = UINavigationController(rootViewController: controller)
//navController = UINavigationController(rootViewController: controller)
viewController?.showDetailViewController(controller, sender: self)
navController = controller.navigationController
return
}
if let navController {
navController.popToRootViewController(animated: true)
let report = navController.viewControllers.first as? ReportController
report?.number = vehicle.getNumber()
// navController.popToRootViewController(animated: true)
// let report = navController.viewControllers.first as? ReportController
// report?.number = vehicle.getNumber()
viewController?.showDetailViewController(navController, sender: self)
}
}
func openEvents(vehicle: VehicleDto) {
func openEvents(vehicle: VehicleDto, onUpdate: @escaping (VehicleDto) -> Void) {
let sb = UIStoryboard(name: "Main", bundle: nil)
let controller = sb.instantiateViewController(identifier: "EventsController") as EventsController
controller.vehicle = self.vehicle
controller.vehicleUpdated = { vehicle in
// TODO: Propagate vehicle update upwards
//self.vehicle = vehicle
}
controller.vehicle = vehicle
controller.vehicleUpdated = onUpdate
navController?.pushViewController(controller, animated: true)
}
@ -76,4 +76,35 @@ class ReportCoordinator: Coordinator {
try? await coordiantor.start()
}
}
func openNotes(vehicle: VehicleDto) async -> VehicleDto? {
guard let navController else {
return nil
}
let coordinator = NotesCoordinator(navController: navController, vehicle: vehicle)
return try? await coordinator.start()
}
func openAds(_ ads: [VehicleAdDto]) {
guard let navController else {
return
}
Task {
let coordinator = AdsCoordinator(navController: navController, ads: ads)
try? await coordinator.start()
}
}
func openPhotos(_ photos: [VehiclePhotoDto]) {
guard let navController else {
return
}
Task {
let coordinator = GalleryCoordinator(navController: navController, photos: photos)
try? await coordinator.start()
}
}
}

View File

@ -53,17 +53,23 @@ struct ReportScreen: View {
Section("History") {
LabeledContent("Events", value: String(viewModel.vehicle.events.count))
.navigationLink(onTap: viewModel.openEvents)
.navigationLink(isActive: !viewModel.vehicle.events.isEmpty,
onTap: viewModel.openEvents)
LabeledContent("OSAGO", value: String(viewModel.vehicle.osagoContracts.count))
.navigationLink(onTap: viewModel.openOsago)
.navigationLink(isActive: !viewModel.vehicle.osagoContracts.isEmpty,
onTap: viewModel.openOsago)
LabeledContent("Owners", value: String(viewModel.vehicle.ownershipPeriods.count))
.navigationLink(onTap: viewModel.openOwners)
.navigationLink(isActive: !viewModel.vehicle.ownershipPeriods.isEmpty,
onTap: viewModel.openOwners)
LabeledContent("Photos", value: String(viewModel.vehicle.photos.count))
.navigationLink(onTap: viewModel.openPhotoGallery)
.navigationLink(isActive: !viewModel.vehicle.photos.isEmpty,
onTap: viewModel.openPhotoGallery)
LabeledContent("Ads", value: String(viewModel.vehicle.ads.count))
.navigationLink(onTap: viewModel.openAds)
.navigationLink(isActive: !viewModel.vehicle.ads.isEmpty,
onTap: viewModel.openAds)
LabeledContent("Notes", value: String(viewModel.vehicle.notes.count))
.navigationLink(onTap: viewModel.openNotes)
.navigationLink(isActive: !viewModel.vehicle.notes.isEmpty,
onTap: viewModel.openNotes)
}
if viewModel.showDebugInfo {
@ -86,6 +92,11 @@ struct ReportScreen: View {
Task { await viewModel.onAppear() }
}
.hud($viewModel.hud)
.toolbar {
if let link = viewModel.shareLink {
ShareLink(item: link)
}
}
}
@ViewBuilder

View File

@ -47,6 +47,14 @@ class ReportViewModel: ACHudContainer {
settings.showDebugInfo
}
var shareLink: URL? {
guard let jwt = try? JWT<EmptyPayload>.generate(for: vehicle.getNumber()) else {
return nil
}
return URL(string: Constants.reportLinkBaseURL + "?token=" + jwt)
}
init(vehicle: VehicleDto, isPersistent: Bool) {
self.vehicle = vehicle
self.isPersistent = isPersistent
@ -84,7 +92,9 @@ class ReportViewModel: ACHudContainer {
// MARK: Open detail screens
func openEvents() {
coordinator?.openEvents(vehicle: vehicle)
coordinator?.openEvents(vehicle: vehicle) { [weak self] vehicle in
self?.vehicle = vehicle
}
}
func openOsago() {
@ -96,14 +106,18 @@ class ReportViewModel: ACHudContainer {
}
func openPhotoGallery() {
coordinator?.openPhotos(vehicle.photos)
}
func openNotes() {
Task {
if let vehicle = await coordinator?.openNotes(vehicle: vehicle) {
self.vehicle = vehicle
}
}
}
func openAds() {
coordinator?.openAds(vehicle.ads)
}
}

View File

@ -11,10 +11,14 @@ import SwiftUI
struct NavigationLinkModifier: ViewModifier {
var onTap: (() -> Void)?
var isActive: Bool
func body(content: Content) -> some View {
if let onTap {
if !isActive {
content
.foregroundStyle(.secondary)
} else if let onTap {
HStack(spacing: 0) {
content
Spacer()
@ -33,7 +37,7 @@ struct NavigationLinkModifier: ViewModifier {
extension View {
func navigationLink(onTap: (() -> Void)?) -> some View {
modifier(NavigationLinkModifier(onTap: onTap))
func navigationLink(isActive: Bool = true, onTap: (() -> Void)?) -> some View {
modifier(NavigationLinkModifier(onTap: onTap, isActive: isActive))
}
}

View File

@ -8,13 +8,19 @@
import Foundation
public struct VehiclePhotoDto: Decodable, Sendable, Equatable {
public struct VehiclePhotoDto: Decodable, Sendable, Equatable, Identifiable {
public let id = UUID()
public var brand: String?
public var model: String?
public var date: TimeInterval = 0
public var url: String = ""
enum CodingKeys: String, CodingKey {
case brand, model, date, url
}
public var description: String {
let formatter = DateFormatter()
formatter.timeZone = TimeZone(identifier:"GMT")
@ -24,4 +30,15 @@ public struct VehiclePhotoDto: Decodable, Sendable, Equatable {
let dateStr = formatter.string(from: date)
return "\(self.brand ?? "") \(self.model ?? "") (\(dateStr))"
}
public init(brand: String? = nil,
model: String? = nil,
date: TimeInterval,
url: String) {
self.brand = brand
self.model = model
self.date = date
self.url = url
}
}