diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index f9d8fd9..46b9c57 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -194,6 +194,7 @@ 7AF6D22A2677C3AD0086EA64 /* Exportable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE8424D26109F78002F6B31 /* Exportable.swift */; }; 7AF8606C2CB9B20C00954D2F /* Mockable in Frameworks */ = {isa = PBXBuildFile; productRef = 7AF8606B2CB9B20C00954D2F /* Mockable */; }; 7AF8606E2CB9B86300954D2F /* Mockable in Frameworks */ = {isa = PBXBuildFile; productRef = 7AF8606D2CB9B86300954D2F /* Mockable */; }; + 7AF860702CBAA24500954D2F /* NavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF8606F2CBAA24500954D2F /* NavigationLink.swift */; }; 7AFBE8C02C3024E5003C491D /* ACHud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE8BF2C3024E5003C491D /* ACHud.swift */; }; 7AFBE8C42C302561003C491D /* ACHudContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE8C32C302561003C491D /* ACHudContainer.swift */; }; 7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE8C92C3081C7003C491D /* ACProgressHud+Modifiers.swift */; }; @@ -437,6 +438,7 @@ 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AutoCatCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AF6D1F12677C03B0086EA64 /* AutoCatCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AutoCatCore.h; sourceTree = ""; }; 7AF6D1F22677C03B0086EA64 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7AF8606F2CBAA24500954D2F /* NavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationLink.swift; sourceTree = ""; }; 7AFBE8BF2C3024E5003C491D /* ACHud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACHud.swift; sourceTree = ""; }; 7AFBE8C32C302561003C491D /* ACHudContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACHudContainer.swift; sourceTree = ""; }; 7AFBE8C92C3081C7003C491D /* ACProgressHud+Modifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ACProgressHud+Modifiers.swift"; sourceTree = ""; }; @@ -1035,6 +1037,7 @@ 7AAAFAD92C4D1AFE0050410D /* Zoomable.swift */, 7A1022712C554A1300B84627 /* CustomHostingController.swift */, 7A5D7E0B2C71EB25002C17E7 /* ToggleRowView.swift */, + 7AF8606F2CBAA24500954D2F /* NavigationLink.swift */, ); path = SwiftUI; sourceTree = ""; @@ -1293,6 +1296,7 @@ 7AC3555029696D5A00889457 /* NewNumberController.swift in Sources */, 7AE26A3524F31B0700625033 /* EventsController.swift in Sources */, 7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */, + 7AF860702CBAA24500954D2F /* NavigationLink.swift in Sources */, 7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */, 7A17CE4A2A2E820300626A6E /* UIStackView.swift in Sources */, 7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */, diff --git a/AutoCat/Controllers/FiltersController.swift b/AutoCat/Controllers/FiltersController.swift index 9b62d1f..5349ae0 100644 --- a/AutoCat/Controllers/FiltersController.swift +++ b/AutoCat/Controllers/FiltersController.swift @@ -35,7 +35,7 @@ class FiltersController: FormViewController { let brandRow = PushRow("Brand") { row in row.title = NSLocalizedString("Brand", comment: "") - row.value = self.filter.brand ?? "Any" + row.value = self.filter.brand.text row.selectorTitle = NSLocalizedString("Brands", comment: "") row.optionsProvider = .lazy({ form, completion in self.runAsync { @@ -45,32 +45,32 @@ class FiltersController: FormViewController { }) } .onPresent(removeSectionName(from:to:)) - .onChange { self.filter.brand = $0.value == "Any" ? nil : $0.value } - .cellUpdate { $1.value = self.filter.brand ?? "Any" } + .onChange { self.filter.brand = .init($0.value) } + .cellUpdate { $1.value = self.filter.brand.text } let modelRow = PushRow("Model") { row in row.title = NSLocalizedString("Model", comment: "") - row.value = self.filter.model ?? "Any" + row.value = self.filter.model.text row.disabled = "$Brand == 'Any'" row.optionsProvider = .lazy({ form, completion in - guard let brand = self.filter.brand else { + guard self.filter.brand != .any else { completion(["Any"]) return } self.runAsync { - let models = try await ApiService.shared.getModels(of: brand) + let models = try await ApiService.shared.getModels(of: self.filter.brand.text) completion(["Any"] + models) } }) } .onPresent(removeSectionName(from:to:)) - .onChange { self.filter.model = $0.value == "Any" ? nil : $0.value } - .cellUpdate { $1.value = self.filter.model ?? "Any" } + .onChange { self.filter.model = .init($0.value) } + .cellUpdate { $1.value = self.filter.model.text } let colorRow = PushRow("Color") { row in row.title = NSLocalizedString("Color", comment: "") - row.value = self.filter.color ?? "Any" + row.value = self.filter.color.text row.optionsProvider = .lazy({ form, completion in self.runAsync { let colors = try await ApiService.shared.getColors() @@ -79,12 +79,12 @@ class FiltersController: FormViewController { }) } .onPresent(removeSectionName(from:to:)) - .onChange { self.filter.color = $0.value == "Any" ? nil : $0.value } - .cellUpdate { $1.value = self.filter.color ?? "Any" } + .onChange { self.filter.color = .init($0.value) } + .cellUpdate { $1.value = self.filter.color.text } let yearRow = PushRow("Year") { row in row.title = NSLocalizedString("Year", comment: "Manufacturing year") - row.value = self.filter.year ?? "Any" + row.value = self.filter.year.text row.optionsProvider = .lazy({ form, completion in self.runAsync { let years = try await ApiService.shared.getYears() @@ -92,8 +92,8 @@ class FiltersController: FormViewController { } }) } - .onChange { self.filter.year = $0.value == "Any" ? nil : $0.value } - .cellUpdate { $1.value = self.filter.year ?? "Any" } + .onChange { self.filter.year = .init($0.value) } + .cellUpdate { $1.value = self.filter.year.text } let mainSection = Section(NSLocalizedString("Main filters", comment: "")) { $0.tag = "MainFilters" } diff --git a/AutoCat/Controllers/SearchController.swift b/AutoCat/Controllers/SearchController.swift index c3cbe98..7eddaa7 100644 --- a/AutoCat/Controllers/SearchController.swift +++ b/AutoCat/Controllers/SearchController.swift @@ -184,26 +184,26 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe } func showFilter() async throws { - let sb = UIStoryboard(name: "Main", bundle: nil) - let controller = sb.instantiateViewController(identifier: "FiltersController") as FiltersController - controller.filter = self.filter - controller.onDone = { - self.filter = controller.filter - self.datasource.setSortParameter(self.filter.sortBy ?? .updatedDate) - self.filter.needReset = true - self.filter.scope = SearchScope(rawValue: self.searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber - self.updateSearchResults(with: self.filter) - } - self.navigationController?.pushViewController(controller, animated: true) - -// if let navigationController = self.navigationController { -// let coordinator = FiltersCoordinator(navController: navigationController, filter: filter) -// filter = try await coordinator.start() -// datasource.setSortParameter(self.filter.sortBy ?? .updatedDate) -// filter.needReset = true -// filter.scope = SearchScope(rawValue: self.searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber -// updateSearchResults(with: self.filter) +// let sb = UIStoryboard(name: "Main", bundle: nil) +// let controller = sb.instantiateViewController(identifier: "FiltersController") as FiltersController +// controller.filter = self.filter +// controller.onDone = { +// self.filter = controller.filter +// self.datasource.setSortParameter(self.filter.sortBy ?? .updatedDate) +// self.filter.needReset = true +// self.filter.scope = SearchScope(rawValue: self.searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber +// self.updateSearchResults(with: self.filter) // } +// self.navigationController?.pushViewController(controller, animated: true) + + if let navigationController = self.navigationController { + let coordinator = FiltersCoordinator(navController: navigationController, filter: filter) + filter = try await coordinator.start() + datasource.setSortParameter(self.filter.sortBy ?? .updatedDate) + filter.needReset = true + filter.scope = SearchScope(rawValue: self.searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber + updateSearchResults(with: self.filter) + } } func showOnMap() { diff --git a/AutoCat/Preview/ApiServiceStub.swift b/AutoCat/Preview/ApiServiceStub.swift index 410009f..7c03fd9 100644 --- a/AutoCat/Preview/ApiServiceStub.swift +++ b/AutoCat/Preview/ApiServiceStub.swift @@ -23,4 +23,24 @@ actor ApiServiceStub: ApiServiceProtocol { func remove(note id: String) async throws -> VehicleDto { vehicle } + + func getBrands() async throws -> [String] { + [] + } + + func getModels(of brand: String) async throws -> [String] { + [] + } + + func getColors() async throws -> [String] { + [] + } + + func getRegions() async throws -> [AutoCatCore.VehicleRegion] { + [] + } + + func getYears() async throws -> [Int] { + [] + } } diff --git a/AutoCat/Screens/FiltersScreen/FiltersScreen.swift b/AutoCat/Screens/FiltersScreen/FiltersScreen.swift index a83b8d7..9600964 100644 --- a/AutoCat/Screens/FiltersScreen/FiltersScreen.swift +++ b/AutoCat/Screens/FiltersScreen/FiltersScreen.swift @@ -24,20 +24,30 @@ struct FiltersScreen: View { var body: some View { Form { Section("Main filters") { - NavigationLink(value: DetailScreen.brand) { - LabeledContent("Brand", value: viewModel.filter.brand ?? "Any") + Picker("Brand", selection: $viewModel.filter.brand) { + ForEach(viewModel.brands, id: \.self) { Text($0.text) } + } + + Picker("Model", selection: $viewModel.filter.model) { + ForEach(viewModel.models, id: \.self) { Text($0.text) } + } + + Picker("Color", selection: $viewModel.filter.color) { + ForEach(viewModel.colors, id: \.self) { Text($0.text) } + } + + Picker("Year", selection: $viewModel.filter.year) { + ForEach(viewModel.years, id: \.self) { Text($0.text) } } - LabeledContent("Model", value: viewModel.filter.model ?? "Any") - LabeledContent("Color", value: viewModel.filter.color ?? "Any") - LabeledContent("Year", value: viewModel.filter.year ?? "Any") } + .pickerStyle(.navigationLink) } .navigationTitle("Filters") .navigationBarItems(trailing: Button("Done", action: { })) - .navigationDestination(for: DetailScreen.self) { _ in - EmptyView() + .task { + await viewModel.loadData() } } } diff --git a/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift b/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift index f46edb5..b478f7a 100644 --- a/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift +++ b/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift @@ -13,9 +13,42 @@ import AutoCatCore @Observable class FiltersViewModel { - var filter: Filter + @ObservationIgnored @Service var api: ApiServiceProtocol + + var filter: Filter { + didSet { + if filter.brand != currentBrand { + if filter.brand != .any { + Task { await loadModels(brand: filter.brand.text) } + } + currentBrand = filter.brand + } + } + } + + var models: [StringOption] = [.any] + var brands: [StringOption] = [.any] + var colors: [StringOption] = [.any] + var years: [StringOption] = [.any] + + @ObservationIgnored var currentBrand: StringOption = .any init(filter: Filter) { self.filter = filter } + + func loadData() async { + guard brands == [.any] || colors == [.any] || years == [.any] else { + return + } + + brands = [.any] + ((try? await api.getBrands()) ?? []).map { .value($0) } + colors = [.any] + ((try? await api.getColors()) ?? []).map { .value($0) } + years = [.any] + ((try? await api.getYears())?.map(String.init) ?? []).map { .value($0) } + } + + func loadModels(brand: String) async { + models = [.any] + ((try? await api.getModels(of: brand)) ?? []).map { .value($0) } + filter.model = .any + } } diff --git a/AutoCat/SwiftUI/NavigationLink.swift b/AutoCat/SwiftUI/NavigationLink.swift new file mode 100644 index 0000000..c192f3a --- /dev/null +++ b/AutoCat/SwiftUI/NavigationLink.swift @@ -0,0 +1,35 @@ +// +// NavigationLink.swift +// AutoCat +// +// Created by Selim Mustafaev on 12.10.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI + +struct NavigationLinkModifier: ViewModifier { + + var onTap: () -> Void + + func body(content: Content) -> some View { + + HStack(spacing: 0) { + content + Spacer() + Image(systemName: "chevron.right") + .font(.footnote) + .fontWeight(.semibold) + .foregroundColor(.secondary) + } + .contentShape(Rectangle()) + .onTapGesture(perform: onTap) + } +} + +extension View { + + func navigationLink(onTap: @escaping () -> Void) -> some View { + modifier(NavigationLinkModifier(onTap: onTap)) + } +} diff --git a/AutoCatCore/Models/Filter.swift b/AutoCatCore/Models/Filter.swift index 4f0c2e2..46bc809 100644 --- a/AutoCatCore/Models/Filter.swift +++ b/AutoCatCore/Models/Filter.swift @@ -61,12 +61,39 @@ public enum SearchScope: Int, CaseIterable, Sendable { } } +public enum StringOption: Hashable, Sendable { + + case any + case value(String) + + public var text: String { + switch self { + case .any: return "Any" + case .value(let value): return value + } + } + + public init(_ text: String?) { + guard let text else { + self = .any + return + } + + switch text { + case "Any": + self = .any + default : + self = .value(text) + } + } +} + public struct Filter: Sendable { public var searchString = "" - public var brand: String? - public var model: String? - public var color: String? - public var year: String? + public var brand: StringOption = .any + public var model: StringOption = .any + public var color: StringOption = .any + public var year: StringOption = .any public var regions: [Int]? public var addedBy: AddedBy? public var sortBy: SortParameter? = .updatedDate @@ -84,10 +111,10 @@ public struct Filter: Sendable { } public mutating func clear() { - self.brand = nil - self.model = nil - self.color = nil - self.year = nil + self.brand = .any + self.model = .any + self.color = .any + self.year = .any self.regions = nil self.addedBy = nil self.sortBy = .updatedDate @@ -104,16 +131,16 @@ public struct Filter: Sendable { public func queryDictionary() -> [String: String] { var dict: [String: String] = ["query": self.searchString] - if let brand = self.brand { + if case let .value(brand) = brand { dict["brand"] = brand } - if let model = self.model { + if case let .value(model) = model { dict["model"] = model } - if let color = self.color { + if case let .value(color) = color { dict["color"] = color } - if let year = self.year { + if case let .value(year) = year { dict["year"] = year } if let regions = self.regions { diff --git a/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift b/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift index 652aa57..8e9483c 100644 --- a/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift +++ b/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift @@ -14,4 +14,10 @@ public protocol ApiServiceProtocol: Sendable { func add(notes: [VehicleNoteDto], to number: String) async throws -> VehicleDto func edit(note: VehicleNoteDto) async throws -> VehicleDto func remove(note id: String) async throws -> VehicleDto + + func getBrands() async throws -> [String] + func getModels(of brand: String) async throws -> [String] + func getColors() async throws -> [String] + func getRegions() async throws -> [VehicleRegion] + func getYears() async throws -> [Int] }