Adding split view for ipad/mac

This commit is contained in:
Selim Mustafaev 2025-04-20 14:46:35 +03:00
parent e7a9f25845
commit 544f81be5e
15 changed files with 347 additions and 416 deletions

View File

@ -37,7 +37,6 @@
7A2C96122C3B155B00AE46B5 /* NoteAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2C96112C3B155B00AE46B5 /* NoteAlertModifier.swift */; };
7A2E11292CCE395300E5CA17 /* OptionalDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E11282CCE395300E5CA17 /* OptionalDatePicker.swift */; };
7A2E6FA72C42B3AD00C40DA7 /* AutoCatCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; };
7A3399AB299063370087DF98 /* SearchControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3399AA299063370087DF98 /* SearchControllerExt.swift */; };
7A386A402DABDC190051676A /* MapScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A386A3F2DABDC190051676A /* MapScreen.swift */; };
7A386A442DABDC360051676A /* MapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A386A432DABDC360051676A /* MapViewModel.swift */; };
7A386A482DABE0D00051676A /* MapMarkerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A386A472DABE0D00051676A /* MapMarkerModel.swift */; };
@ -162,11 +161,14 @@
7ABDA80F2D8723F90083C715 /* StorageService+AudioRecords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABDA80E2D8723F90083C715 /* StorageService+AudioRecords.swift */; };
7AC3554A2969652F00889457 /* SwiftEntryKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7AC355492969652F00889457 /* SwiftEntryKit */; };
7AC3555029696D5A00889457 /* NewNumberController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3554F29696D5A00889457 /* NewNumberController.swift */; };
7AC3555229696E3F00889457 /* UIView+layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3555129696E3F00889457 /* UIView+layout.swift */; };
7AC35554296973E100889457 /* ACButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC35553296973E100889457 /* ACButton.swift */; };
7AC355592969746600889457 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC355582969746600889457 /* UIControl.swift */; };
7AC3555B296995B200889457 /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3555A296995B200889457 /* UIEdgeInsets.swift */; };
7AC44B822DB390B900ADC026 /* MainTabScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC44B812DB390B900ADC026 /* MainTabScreen.swift */; };
7AC44B842DB3EF7A00ADC026 /* HistorySplitScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC44B832DB3EF7A00ADC026 /* HistorySplitScreen.swift */; };
7AC44B862DB40A5C00ADC026 /* HideTabBarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC44B852DB40A5C00ADC026 /* HideTabBarModifier.swift */; };
7AC44B882DB438F200ADC026 /* UIView+layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC44B872DB438F200ADC026 /* UIView+layout.swift */; };
7AC44B8A2DB4395300ADC026 /* SearchSplitScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC44B892DB4395300ADC026 /* SearchSplitScreen.swift */; };
7AC76D7B270083AE0084DB27 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC76D7A270083AE0084DB27 /* TextView.swift */; };
7AC8B2762D6A01C700190706 /* UISearchTextField+Dumb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC8B2752D6A01C700190706 /* UISearchTextField+Dumb.swift */; };
7ACBB91E2CB9B155005A5168 /* Mockable in Frameworks */ = {isa = PBXBuildFile; productRef = 7ACBB91D2CB9B155005A5168 /* Mockable */; };
@ -305,7 +307,6 @@
7A2E11282CCE395300E5CA17 /* OptionalDatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalDatePicker.swift; sourceTree = "<group>"; };
7A2E6FA32C42B3AD00C40DA7 /* AutoCatCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AutoCatCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
7A333813249A532400D878F1 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = "<group>"; };
7A3399AA299063370087DF98 /* SearchControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchControllerExt.swift; sourceTree = "<group>"; };
7A386A3F2DABDC190051676A /* MapScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapScreen.swift; sourceTree = "<group>"; };
7A386A432DABDC360051676A /* MapViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewModel.swift; sourceTree = "<group>"; };
7A386A472DABE0D00051676A /* MapMarkerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapMarkerModel.swift; sourceTree = "<group>"; };
@ -433,11 +434,14 @@
7ABDA80C2D8721B10083C715 /* Substrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Substrings.swift; sourceTree = "<group>"; };
7ABDA80E2D8723F90083C715 /* StorageService+AudioRecords.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageService+AudioRecords.swift"; sourceTree = "<group>"; };
7AC3554F29696D5A00889457 /* NewNumberController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNumberController.swift; sourceTree = "<group>"; };
7AC3555129696E3F00889457 /* UIView+layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+layout.swift"; sourceTree = "<group>"; };
7AC35553296973E100889457 /* ACButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACButton.swift; sourceTree = "<group>"; };
7AC355582969746600889457 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; };
7AC3555A296995B200889457 /* UIEdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIEdgeInsets.swift; sourceTree = "<group>"; };
7AC44B812DB390B900ADC026 /* MainTabScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabScreen.swift; sourceTree = "<group>"; };
7AC44B832DB3EF7A00ADC026 /* HistorySplitScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySplitScreen.swift; sourceTree = "<group>"; };
7AC44B852DB40A5C00ADC026 /* HideTabBarModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HideTabBarModifier.swift; sourceTree = "<group>"; };
7AC44B872DB438F200ADC026 /* UIView+layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+layout.swift"; sourceTree = "<group>"; };
7AC44B892DB4395300ADC026 /* SearchSplitScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSplitScreen.swift; sourceTree = "<group>"; };
7AC76D7A270083AE0084DB27 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = "<group>"; };
7AC8B2752D6A01C700190706 /* UISearchTextField+Dumb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISearchTextField+Dumb.swift"; sourceTree = "<group>"; };
7ADF6C92250B954900F237B2 /* Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = "<group>"; };
@ -676,6 +680,7 @@
7A131FD12D37B74100DC7755 /* HistoryScreen */ = {
isa = PBXGroup;
children = (
7AC44B832DB3EF7A00ADC026 /* HistorySplitScreen.swift */,
7A131FD22D37B75500DC7755 /* HistoryScreen.swift */,
7A131FD42D37B76A00DC7755 /* HistoryViewModel.swift */,
7A4955812D58CCF900912E66 /* HistoryFilter.swift */,
@ -789,6 +794,7 @@
7A5911EC2D63225500EC51BA /* SearchScreen */ = {
isa = PBXGroup;
children = (
7AC44B892DB4395300ADC026 /* SearchSplitScreen.swift */,
7A5911ED2D63226F00EC51BA /* SearchScreen.swift */,
7A5911EF2D63266B00EC51BA /* SearchViewModel.swift */,
);
@ -1069,13 +1075,12 @@
7AC355572969744100889457 /* Extensions */ = {
isa = PBXGroup;
children = (
7A3399AA299063370087DF98 /* SearchControllerExt.swift */,
7AC3555129696E3F00889457 /* UIView+layout.swift */,
7AC355582969746600889457 /* UIControl.swift */,
7AC3555A296995B200889457 /* UIEdgeInsets.swift */,
7A91894E29A2BD8700519C74 /* GestureRecognizers.swift */,
7A17CE492A2E820300626A6E /* UIStackView.swift */,
7A17CE4B2A2E850200626A6E /* UISegmentedControl.swift */,
7AC44B872DB438F200ADC026 /* UIView+layout.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1169,6 +1174,7 @@
7AB4902A2D6B1446002F39C6 /* ACKeyboardButton.swift */,
7A589E0E2D6B6E8E00EF3FBE /* NumberEditView.swift */,
7AF231982DA27C1B00AE5EB3 /* ACButtonView.swift */,
7AC44B852DB40A5C00ADC026 /* HideTabBarModifier.swift */,
);
path = SwiftUI;
sourceTree = "<group>";
@ -1408,11 +1414,11 @@
7A1441662C297EDE00E79018 /* NotesScreen.swift in Sources */,
7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */,
7A1E78FF2CE91A740004B740 /* Vehicle.swift in Sources */,
7A3399AB299063370087DF98 /* SearchControllerExt.swift in Sources */,
7A14416E2C297F7C00E79018 /* Coordinator.swift in Sources */,
7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */,
7A1441682C297EFD00E79018 /* NotesViewModel.swift in Sources */,
7AFBE8C02C3024E5003C491D /* ACHud.swift in Sources */,
7AC44B8A2DB4395300ADC026 /* SearchSplitScreen.swift in Sources */,
7AAAFADA2C4D1AFE0050410D /* Zoomable.swift in Sources */,
7ADFC9572DAD0288001A43E3 /* GoogleAuthScreen.swift in Sources */,
7AC8B2762D6A01C700190706 /* UISearchTextField+Dumb.swift in Sources */,
@ -1430,12 +1436,14 @@
7A17CE4A2A2E820300626A6E /* UIStackView.swift in Sources */,
7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */,
7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */,
7AC44B882DB438F200ADC026 /* UIView+layout.swift in Sources */,
7A6C65222D999325001240C2 /* AudioRecordViewModel.swift in Sources */,
7A06E0AE2C7065C7005731AC /* SettingsViewModel.swift in Sources */,
7AB4E42C2D397D8E0006D052 /* VehicleCellView.swift in Sources */,
7A961C6E2C4C3C9E00CE2211 /* LinkRowView.swift in Sources */,
7A8A2209248D10EC0073DFD9 /* ResizeImage.swift in Sources */,
7ADF6CA12512244400F237B2 /* MapExt.swift in Sources */,
7AC44B842DB3EF7A00ADC026 /* HistorySplitScreen.swift in Sources */,
7AC44B822DB390B900ADC026 /* MainTabScreen.swift in Sources */,
7A7158122C444A6400852088 /* AdsViewModel.swift in Sources */,
7AF231952DA1C29300AE5EB3 /* AuthViewModel.swift in Sources */,
@ -1471,7 +1479,6 @@
7AFBE8CE2C308B53003C491D /* ACMessageView.swift in Sources */,
7AAAFADE2C4D23620050410D /* ACImageSliderModel.swift in Sources */,
7A8AB76525A0DB8F00ECF2C1 /* BundleVersion.swift in Sources */,
7AC3555229696E3F00889457 /* UIView+layout.swift in Sources */,
7A9519822D80B6E500E69883 /* RecordsViewModel.swift in Sources */,
7AC355592969746600889457 /* UIControl.swift in Sources */,
7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */,
@ -1495,6 +1502,7 @@
7A5911F02D63266B00EC51BA /* SearchViewModel.swift in Sources */,
7A589E0F2D6B6E8E00EF3FBE /* NumberEditView.swift in Sources */,
7ADF6C97250F41B000F237B2 /* PNKeyboard.swift in Sources */,
7AC44B862DB40A5C00ADC026 /* HideTabBarModifier.swift in Sources */,
7A17CE4C2A2E850200626A6E /* UISegmentedControl.swift in Sources */,
7A9519802D80B6C100E69883 /* RecordsScreen.swift in Sources */,
7A131FD52D37B76A00DC7755 /* HistoryViewModel.swift in Sources */,

View File

@ -1,57 +0,0 @@
//
// SearchControllerExt.swift
// AutoCat
//
// Created by Selim Mustafaev on 06.02.2023.
// Copyright © 2023 Selim Mustafaev. All rights reserved.
//
import UIKit
extension UISearchController {
static var `default`: UISearchController {
let searchController = UISearchController(searchResultsController: nil)
searchController.obscuresBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
searchController.searchBar.keyboardType = .webSearch
if #available(iOS 16, *) {
searchController.scopeBarActivation = .onTextEntry
} else {
searchController.automaticallyShowsScopeBar = true
}
return searchController
}
func placeholder(_ placeholder: String) -> UISearchController {
searchBar.placeholder = placeholder
return self
}
func resultsUpdater(_ updater: UISearchResultsUpdating) -> UISearchController {
searchResultsUpdater = updater
return self
}
func makeDumb() -> UISearchController {
searchBar.autocorrectionType = .no
searchBar.spellCheckingType = .no
searchBar.autocapitalizationType = .none
searchBar.smartDashesType = .no
searchBar.smartQuotesType = .no
searchBar.smartInsertDeleteType = .no
return self
}
func scopeButtons(_ buttons: [String]) -> UISearchController {
searchBar.scopeButtonTitles = buttons
return self
}
func searchBarDelegate(_ delegate: UISearchBarDelegate) -> UISearchController {
searchBar.delegate = delegate
return self
}
}

View File

@ -10,24 +10,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
HUD.dimsBackground = true
HUD.allowsInteraction = false
setupAppearance()
return true
}
func setupAppearance() {
let tabBarAppearance = UITabBarAppearance()
tabBarAppearance.configureWithDefaultBackground()
UITabBar.appearance().standardAppearance = tabBarAppearance
UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
let navigationBarAppearance = UINavigationBarAppearance()
navigationBarAppearance.configureWithDefaultBackground()
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
@ -42,30 +27,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
// MARK: - Global menu
#if targetEnvironment(macCatalyst)
override func buildMenu(with builder: UIMenuBuilder) {
super.buildMenu(with: builder)
let command = UIKeyCommand(title: NSLocalizedString("Add new number", comment: ""),
action: #selector(openAddNumberPanel),
input: "n",
modifierFlags: .command)
let menu = UIMenu(options: .displayInline, children: [command])
builder.insertChild(menu, atEndOfMenu: .file)
}
@objc func openAddNumberPanel() {
guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else {
return
}
sceneDelegate.showAddNumberPanel()
}
#endif
}

View File

@ -97,21 +97,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
}
#if targetEnvironment(macCatalyst)
let toolbar = NSToolbar(identifier: "main")
toolbar.delegate = self
toolbar.displayMode = .iconOnly
if let titlebar = windowScene.titlebar {
titlebar.toolbar = toolbar
if #available(macCatalyst 14.0, *) {
titlebar.toolbarStyle = .unifiedCompact
}
}
#endif
self.window?.makeKeyAndVisible()
}
@ -180,74 +165,3 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
}
}
#if targetEnvironment(macCatalyst)
extension NSToolbarItem.Identifier {
static let addNumber = NSToolbarItem.Identifier("pro.aliencat.add.number")
}
extension SceneDelegate: NSToolbarDelegate {
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
let identifiers: [NSToolbarItem.Identifier] = [
.addNumber
]
return identifiers
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return toolbarDefaultItemIdentifiers(toolbar)
}
func toolbar(_ toolbar: NSToolbar,
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
var toolbarItem: NSToolbarItem?
switch itemIdentifier {
case .addNumber:
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.image = UIImage(systemName: "plus")
item.label = "Add"
item.target = self
item.action = #selector(showAddNumberPanel)
toolbarItem = item
default:
toolbarItem = nil
}
return toolbarItem
}
@objc func showAddNumberPanel() {
let controller = NewNumberController()
controller.preferredContentSize = CGSize(width: 400, height: 350)
controller.onCheck = { number in
controller.dismiss(animated: true) { [weak self] in
self?.checkNewNumber(number)
}
}
self.window?.rootViewController?.present(controller, animated: true)
}
func checkNewNumber(_ number: String) {
guard let split = self.window?.rootViewController as? MainSplitController,
let tabvc = split.viewControllers.first as? MainTabController
else {
return
}
tabvc.selectedIndex = 0
Task {
await tabvc.historyViewModel?.checkNewNumber(number)
}
}
}
#endif

View File

@ -20,6 +20,7 @@ enum DetailScreen: Hashable {
struct FiltersScreen: View {
@Environment(\.dismiss) var dismiss
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State var viewModel: FiltersViewModel
@ -113,6 +114,7 @@ struct FiltersScreen: View {
}
}
.navigationTitle("Filters")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
@ -120,10 +122,18 @@ struct FiltersScreen: View {
dismiss()
}
}
if horizontalSizeClass == .regular {
ToolbarItem(placement: .topBarLeading) {
Button("Close") {
dismiss()
}
}
}
}
.task {
await viewModel.loadData()
}
.toolbar(.hidden, for: .tabBar)
.hideTabBar()
}
}

View File

@ -15,28 +15,17 @@ struct HistoryScreen: View {
@State var filterSheetPresented = false
@State var exportSheetPresented = false
init() {
let resolver = ServiceContainer.shared
self.viewModel = HistoryViewModel(
apiService: resolver.resolve(ApiServiceProtocol.self),
storageService: resolver.resolve(StorageServiceProtocol.self),
vehicleService: resolver.resolve(VehicleServiceProtocol.self)
)
}
init(viewModel: HistoryViewModel) {
self.viewModel = viewModel
}
var body: some View {
NavigationStack {
List {
List(selection: $viewModel.selectedVehicleId) {
ForEach(viewModel.vehicleSections) { section in
Section(header: Text(section.header)) {
ForEach(section.elements) { vehicle in
NavigationLink(value: vehicle) {
NavigationLink(value: vehicle.id) {
vehicleCell(vehicle)
}
}
@ -87,21 +76,6 @@ struct HistoryScreen: View {
}
}
}
.navigationDestination(for: VehicleDto.self) { vehicle in
ReportScreen(
vehicle: vehicle,
isPersistent: true,
onUpdate: viewModel.onVehicleChanged
)
}
.navigationDestination(item: $viewModel.vehicleToOpen) { vehicle in
ReportScreen(
vehicle: vehicle,
isPersistent: true,
onUpdate: viewModel.onVehicleChanged
)
}
}
}
func vehicleCell(_ vehicle: VehicleDto) -> some View {

View File

@ -0,0 +1,29 @@
//
// HistorySplitScreen.swift
// AutoCat
//
// Created by Selim Mustafaev on 19.04.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import SwiftUI
import AutoCatCore
struct HistorySplitScreen: View {
var viewModel: HistoryViewModel
var body: some View {
NavigationSplitView {
HistoryScreen(viewModel: viewModel)
} detail: {
if let vehicle = viewModel.selectedVehicle {
ReportScreen(vehicle: vehicle, isPersistent: true, onUpdate: viewModel.onVehicleChanged)
.id(vehicle.id)
} else {
Text("No vehicle selected")
}
}
.navigationSplitViewStyle(.balanced)
}
}

View File

@ -30,7 +30,19 @@ final class HistoryViewModel: ACHudContainer {
var vehiclesFiltered: [VehicleDto] = []
var vehicleSections: [DateSection<VehicleDto>] = []
var vehicleToOpen: VehicleDto?
var selectedVehicleId: VehicleDto.ID?
var selectedVehicle: VehicleDto? {
guard let id = selectedVehicleId else {
return nil
}
if let vehicle = vehicles.first(where: { $0.id == id }) {
return vehicle
} else {
return nil
}
}
var searchText: String = "" {
didSet {
@ -130,7 +142,7 @@ final class HistoryViewModel: ACHudContainer {
hud = nil
if !vehicle.unrecognized {
// TODO: Fix programmatic navigation
//vehicleToOpen = vehicle
//selectedVehicleId = vehicle.id
}
} else {
showErrors(errors)

View File

@ -38,7 +38,7 @@ struct MainTabScreen: View {
var body: some View {
TabView(selection: $selection) {
HistoryScreen(viewModel: historyViewModel)
HistorySplitScreen(viewModel: historyViewModel)
.tabItem {
Label("History", systemImage: "clock.arrow.circlepath")
}
@ -58,7 +58,7 @@ struct MainTabScreen: View {
.tag(2)
#endif
SearchScreen()
SearchSplitScreen()
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
@ -115,7 +115,3 @@ struct MainTabScreen: View {
Task { await historyViewModel.checkRecord(number: number, event: event) }
}
}
#Preview {
MainTabScreen()
}

View File

@ -13,6 +13,9 @@ import AutoCatCore
struct MapScreen: View {
@Environment(\.dismiss) var dismiss
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State var viewModel: MapViewModel
init(mapInput: MapInput) {
@ -37,6 +40,7 @@ struct MapScreen: View {
}
.hud($viewModel.hud)
.navigationTitle(viewModel.title)
.navigationBarTitleDisplayMode(.inline)
.readSize { newValue in
viewModel.mapSize = newValue
}
@ -48,6 +52,15 @@ struct MapScreen: View {
await viewModel.reloadMarkers()
}
}
.toolbar(.hidden, for: .tabBar)
.toolbar {
if horizontalSizeClass == .regular {
ToolbarItem(placement: .topBarLeading) {
Button("Close") {
dismiss()
}
}
}
}
.hideTabBar()
}
}

View File

@ -37,6 +37,7 @@ struct ReportScreen: View {
}
var body: some View {
NavigationStack {
Form {
Section {
LabeledContent {
@ -146,7 +147,8 @@ struct ReportScreen: View {
GalleryScreen(photos: viewModel.vehicle.photos)
}
}
.toolbar(.hidden, for: .tabBar)
.hideTabBar()
}
}
@ViewBuilder

View File

@ -13,25 +13,19 @@ struct SearchScreen: View {
enum Screen: Hashable {
case report(VehicleDto)
case filter(Filter)
case map(Filter)
}
@State var viewModel: SearchViewModel
init() {
init(viewModel: SearchViewModel) {
let resolver = ServiceContainer.shared
self.viewModel = SearchViewModel(
apiService: resolver.resolve(ApiServiceProtocol.self),
vehicleService: resolver.resolve(VehicleServiceProtocol.self)
)
self.viewModel = viewModel
}
var body: some View {
NavigationStack {
List {
List(selection: $viewModel.selectedVehicleId) {
vehicles
if viewModel.hasMoreData && !viewModel.vehicleSections.isEmpty {
progressCell
@ -59,12 +53,6 @@ struct SearchScreen: View {
}
.navigationDestination(for: Screen.self) { screen in
switch screen {
case .report(let vehicle):
ReportScreen(
vehicle: vehicle,
isPersistent: false,
onUpdate: viewModel.onVehicleChanged
)
case .filter(let filter):
FiltersScreen(filter: filter, onUpdate: viewModel.onFilterChanged)
case .map(let filter):
@ -72,13 +60,12 @@ struct SearchScreen: View {
}
}
}
}
var vehicles: some View {
ForEach(viewModel.vehicleSections) { section in
Section(header: Text(section.header)) {
ForEach(section.elements) { vehicle in
NavigationLink(value: Screen.report(vehicle)) {
NavigationLink(value: vehicle.id) {
vehicleCell(vehicle)
}
}

View File

@ -0,0 +1,37 @@
//
// SearchSplitScreen.swift
// AutoCat
//
// Created by Selim Mustafaev on 19.04.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import SwiftUI
import AutoCatCore
struct SearchSplitScreen: View {
@State var viewModel: SearchViewModel
init() {
let resolver = ServiceContainer.shared
self.viewModel = SearchViewModel(
apiService: resolver.resolve(ApiServiceProtocol.self),
vehicleService: resolver.resolve(VehicleServiceProtocol.self)
)
}
var body: some View {
NavigationSplitView {
SearchScreen(viewModel: viewModel)
} detail: {
if let vehicle = viewModel.selectedVehicle {
ReportScreen(vehicle: vehicle, isPersistent: false, onUpdate: viewModel.onVehicleChanged)
.id(vehicle.id)
} else {
Text("No vehicle selected")
}
}
.navigationSplitViewStyle(.balanced)
}
}

View File

@ -23,6 +23,20 @@ final class SearchViewModel: ACHudContainer {
var vehicles: [VehicleDto] = []
var vehicleSections: [DateSection<VehicleDto>] = []
var selectedVehicleId: VehicleDto.ID?
var selectedVehicle: VehicleDto? {
guard let id = selectedVehicleId else {
return nil
}
if let vehicle = vehicles.first(where: { $0.id == id }) {
return vehicle
} else {
return nil
}
}
var filter = Filter()
var pageToken: String?
var hasMoreData: Bool = true

View File

@ -0,0 +1,32 @@
//
// HideTabBarModifier.swift
// AutoCat
//
// Created by Selim Mustafaev on 19.04.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import SwiftUI
struct HideTabBarModifier: ViewModifier {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
func body(content: Content) -> some View {
if horizontalSizeClass == .compact {
content
.toolbar(.hidden, for: .tabBar)
} else {
content
}
}
}
extension View {
public func hideTabBar() -> some View {
modifier(HideTabBarModifier())
}
}