Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ScheduleTracker.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Debug;
};
Expand Down Expand Up @@ -274,6 +275,7 @@
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_STRICT_CONCURRENCY = complete;
VALIDATE_PRODUCT = YES;
};
name = Release;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
3 changes: 2 additions & 1 deletion ScheduleTracker/API/APIServicesContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
//
// Created by Василий Ханин on 12.06.2025.
//
import Combine

final class APIServicesContainer {
final class APIServicesContainer: ObservableObject {
let nearestStationsService: NearestStationsServiceProtocol
let copyrightService: CopyrightServiceProtocol
let searchService: SearchServiceProtocol
Expand Down
10 changes: 10 additions & 0 deletions ScheduleTracker/App/ScheduleTrackerApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,21 @@ import SwiftUI

@main
struct ScheduleTrackerApp: App {
private static var servicesInstance: APIServicesContainer = {
do {
return try APIServicesContainer()
} catch {
fatalError("Failed to initialize APIServicesContainer: \(error)")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

По своему опыту хотел бы отметить, что в продакшене не используют fatalError, советую заменить на assertionFailure, тут подробнее

}
}()

@StateObject private var services = servicesInstance
@AppStorage("isDarkMode") private var isDarkMode = false

var body: some Scene {
WindowGroup {
BaseView()
.environmentObject(services)
.preferredColorScheme(isDarkMode ? .dark : .light)
}
}
Expand Down
35 changes: 35 additions & 0 deletions ScheduleTracker/Helpers/AppErrorType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// AppErrorType.swift
// ScheduleTracker
//
// Created by Василий Ханин on 24.07.2025.
//


import Foundation

enum AppErrorType {
case none
case server
case internet
}

@MainActor
protocol ErrorHandleable: ObservableObject {
var errorType: AppErrorType { get set }
}

extension ErrorHandleable {
func handle(error: Error) {
if let urlError = error as? URLError {
switch urlError.code {
case .notConnectedToInternet:
errorType = .internet
default:
errorType = .server
}
} else {
errorType = .server
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ final class NavigationCoordinator: ObservableObject {
@Published var selectedCityTo: String = ""
@Published var selectedStationTo: String = ""
@Published var timeFilters: Set<TimePeriod> = []
@Published var showTransfers: Bool? = nil

@Published var showTransfers: Bool?
@Published var selectedStationFromCode: String = ""
@Published var selectedStationToCode: String = ""

var isFiltersValid: Bool {
!timeFilters.isEmpty && showTransfers != nil
}
Expand Down
31 changes: 31 additions & 0 deletions ScheduleTracker/Helpers/StateWrapperView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// StateWrapperView.swift
// ScheduleTracker
//
// Created by Василий Ханин on 25.07.2025.
//
import SwiftUI

struct StateWrapperView<Content: View>: View {
let isLoading: Bool
let errorType: AppErrorType?
let content: () -> Content

var body: some View {
Group {
if isLoading {
VStack {
Spacer()
ProgressView()
Spacer()
}
} else if errorType == .internet {
ErrorInternetView()
} else if errorType == .server {
ErrorServerView()
} else {
content()
}
}
}
}
18 changes: 18 additions & 0 deletions ScheduleTracker/Models/CarrierInfoModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// CarrierInfoModel.swift
// ScheduleTracker
//
// Created by Василий Ханин on 24.07.2025.
//

import Foundation

struct CarrierInfoModel: Identifiable, Sendable {
var id: Int { code }
let code: Int
let title: String
let logo: String?
let email: String?
let phone: String?
}

12 changes: 12 additions & 0 deletions ScheduleTracker/Models/StationModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// StationModel.swift
// ScheduleTracker
//
// Created by Василий Ханин on 23.07.2025.
//
import Foundation

struct StationModel: Identifiable, Sendable {
let id: String
let title: String
}
2 changes: 1 addition & 1 deletion ScheduleTracker/Models/StoryModel.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SwiftUI

struct Story: Identifiable {
struct Story: Identifiable, Sendable {
let id = UUID()
let previewImageName: String
let fullImageName: String
Expand Down
5 changes: 3 additions & 2 deletions ScheduleTracker/Models/TicketModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import SwiftUI

struct TicketModel: Hashable, Identifiable {
struct TicketModel: Hashable, Identifiable, Sendable {
let id = UUID()
let operatorName: String
let date: String
Expand All @@ -16,5 +16,6 @@ struct TicketModel: Hashable, Identifiable {
let duration: String
let withTransfer: Bool
let operatorLogo: String
let note: String?
let transfer: String?
let carrierCode: Int?
}
4 changes: 2 additions & 2 deletions ScheduleTracker/Services/CarrierServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import OpenAPIURLSession

typealias Carrier = Components.Schemas.CarrierResponse

protocol CarrierServiceProtocol {
@preconcurrency protocol CarrierServiceProtocol {
func getCarrierInfo(code: String) async throws -> Carrier
}

final class CarrierService: CarrierServiceProtocol {
actor CarrierService: CarrierServiceProtocol {

private let client: Client
private let apikey: String
Expand Down
4 changes: 2 additions & 2 deletions ScheduleTracker/Services/CopyrightServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import OpenAPIURLSession

typealias Copyright = Components.Schemas.Copyright

protocol CopyrightServiceProtocol {
@preconcurrency protocol CopyrightServiceProtocol {
func getCopyright() async throws -> Copyright
}

final class CopyrightService: CopyrightServiceProtocol {
actor CopyrightService: CopyrightServiceProtocol {
private let client: Client
private let apikey: String

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import OpenAPIURLSession

typealias Settlement = Components.Schemas.Settlement

protocol NearestSettlementServiceProtocol {
@preconcurrency protocol NearestSettlementServiceProtocol {
func getNearestCity(lat: Double, lng: Double) async throws -> Settlement
}

final class SettlementService: NearestSettlementServiceProtocol {
actor SettlementService: NearestSettlementServiceProtocol {

private let client: Client
private let apikey: String
Expand Down
4 changes: 2 additions & 2 deletions ScheduleTracker/Services/NearestStationsServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ import OpenAPIURLSession
typealias NearestStations = Components.Schemas.Stations

// Определяем протокол для нашего сервиса (хорошая практика для тестирования и гибкости)
protocol NearestStationsServiceProtocol {
@preconcurrency protocol NearestStationsServiceProtocol {
// Функция для получения станций, асинхронная и может выбросить ошибку
func getNearestStations(lat: Double, lng: Double, distance: Int) async throws -> NearestStations
}

// Конкретная реализация сервиса
final class NearestStationsService: NearestStationsServiceProtocol {
actor NearestStationsService: NearestStationsServiceProtocol {
// Хранит экземпляр сгенерированного клиента
private let client: Client
// Хранит API-ключ (лучше передавать его извне, чем хранить прямо в сервисе)
Expand Down
4 changes: 2 additions & 2 deletions ScheduleTracker/Services/ScheduleServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import OpenAPIURLSession

typealias Schedule = Components.Schemas.ScheduleSchema

protocol ScheduleServiceProtocol {
@preconcurrency protocol ScheduleServiceProtocol {
func getStationSchedule(station: String) async throws -> Schedule
}

final class ScheduleService: ScheduleServiceProtocol {
actor ScheduleService: ScheduleServiceProtocol {

private let client: Client
private let apikey: String
Expand Down
8 changes: 4 additions & 4 deletions ScheduleTracker/Services/SearchServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import OpenAPIURLSession

typealias Search = Components.Schemas.SearchSchema

protocol SearchServiceProtocol {
func getSchedualBetweenStations(from: String, to: String) async throws -> Search
@preconcurrency protocol SearchServiceProtocol {
func getScheduleBetweenStations(from: String, to: String) async throws -> Search
}

final class SearchService: SearchServiceProtocol {
actor SearchService: SearchServiceProtocol {

private let client: Client
private let apikey: String
Expand All @@ -24,7 +24,7 @@ final class SearchService: SearchServiceProtocol {
self.apikey = apikey
}

func getSchedualBetweenStations(from: String, to: String) async throws -> Search {
func getScheduleBetweenStations(from: String, to: String) async throws -> Search {
let response = try await client.getSearch(query: .init(apikey: apikey, from: from, to: to))

return try response.ok.body.json
Expand Down
4 changes: 2 additions & 2 deletions ScheduleTracker/Services/StationListServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import Foundation

typealias AllStation = Components.Schemas.StationsList

protocol StationListServiceProtocol {
@preconcurrency protocol StationListServiceProtocol {
func getAllStations() async throws -> AllStation
}

final class StationtService: StationListServiceProtocol {
actor StationtService: StationListServiceProtocol {
private let client: Client
private let apikey: String

Expand Down
4 changes: 2 additions & 2 deletions ScheduleTracker/Services/ThreadServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import OpenAPIURLSession

typealias Thread = Components.Schemas.Thread

protocol ThreadServiceProtocol {
@preconcurrency protocol ThreadServiceProtocol {
func getRouteStations(uid: String) async throws -> Thread
}

final class ThreadService: ThreadServiceProtocol {
actor ThreadService: ThreadServiceProtocol {

private let client: Client
private let apikey: String
Expand Down
46 changes: 46 additions & 0 deletions ScheduleTracker/ViewModels/CarrierInfoViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// CarrierInfoViewModel.swift
// ScheduleTracker
//
// Created by Василий Ханин on 24.07.2025.
//

import Foundation

@MainActor
final class CarrierInfoViewModel: ObservableObject, ErrorHandleable {
@Published var carrier: CarrierInfoModel?
@Published var isLoading = false
@Published var errorMessage: String?
@Published var errorType: AppErrorType = .none

private let service: CarrierServiceProtocol

init(service: CarrierServiceProtocol) {
self.service = service
}

func fetchCarrier(code: String) async {
isLoading = true
errorType = .none
do {
let response = try await service.getCarrierInfo(code: code)
guard let carrier = response.carrier else {
errorMessage = "Не найдено информации о перевозчике"
isLoading = false
return
}
self.carrier = CarrierInfoModel(
code: carrier.code ?? 0,
title: carrier.title ?? "—",
logo: carrier.logo,
email: carrier.email,
phone: carrier.phone
)
} catch {
self.handle(error: error)
}
isLoading = false
}
}

Loading