← Назад к вопросам
Авиаперелеты (Wildberries)
1.8 Middle🔥 81 комментариев
#Архитектура и паттерны#Работа с сетью#Хранение данных
Условие
Разработайте iOS приложение для отображения авиаперелетов.
Функциональные требования:
- Два экрана: список рейсов и детальная информация
- Отображение города отправления и прибытия
- Отображение дат вылета и прилета
- Отображение цены
- Кнопка "Лайк" для добавления в избранное
- Избранные рейсы сохраняются локально
Технические требования:
- Core Data для хранения избранного
- API: travel.wildberries.ru/statistics/v1/cheap (или аналогичное)
- UIKit или SwiftUI
- Архитектура на выбор (MVP, MVVM, VIPER)
Дизайн:
- Карточки рейсов с основной информацией
- Визуальное выделение избранных рейсов
- Детальный экран с полной информацией о рейсе
Тестовое задание от компании Wildberries на позицию iOS разработчика.
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Архитектура проекта (MVVM)
FlightsApp/
├── Models/
│ ├── Flight.swift
│ ├── Airport.swift
│ └── APIModels.swift
├── Core Data/
│ ├── CoreDataStack.swift
│ ├── FavoriteFlight+CoreDataClass.swift
│ ├── FavoriteFlight+CoreDataProperties.swift
│ └── Flights.xcdatamodeld
├── Network/
│ └── FlightsAPIClient.swift
├── Services/
│ ├── FlightService.swift
│ └── FavoritesService.swift
├── ViewModels/
│ ├── FlightsListViewModel.swift
│ └── FlightDetailViewModel.swift
├── Views/
│ ├── FlightsListViewController.swift
│ ├── FlightCell.swift
│ ├── FlightDetailViewController.swift
│ └── LoadingCell.swift
└── Utilities/
└── DateFormatter+Extensions.swift
Domain Models
Models/Flight.swift:
import Foundation
struct Flight: Identifiable, Hashable {
let id: String
let departureAirport: Airport
let arrivalAirport: Airport
let departureTime: Date
let arrivalTime: Date
let price: Int // в рублях
let airline: String
let flightDuration: TimeInterval
let seats: Int
let isFavorite: Bool
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Flight, rhs: Flight) -> Bool {
lhs.id == rhs.id
}
}
struct Airport: Codable, Hashable {
let code: String // IATA: SVO, JFK, CDG
let name: String
let city: String
let country: String
}
struct FlightsResponse: Codable {
let data: [FlightDTO]
}
struct FlightDTO: Codable {
let id: String
let departure: AirportDTO
let arrival: AirportDTO
let departureDate: String // ISO 8601
let arrivalDate: String
let price: Int
let airline: String
let duration: Int // в секундах
let seats: Int
enum CodingKeys: String, CodingKey {
case id
case departure
case arrival
case departureDate = "departure_date"
case arrivalDate = "arrival_date"
case price
case airline
case duration
case seats
}
}
struct AirportDTO: Codable {
let code: String
let name: String
let city: String
let country: String
}
Core Data Setup
Core Data/CoreDataStack.swift:
import CoreData
final class CoreDataStack {
static let shared = CoreDataStack()
private let modelName = "Flights"
private(set) lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: modelName)
container.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Core Data load error: \(error)")
}
}
return container
}()
var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
func save() {
let context = viewContext
if context.hasChanges {
do {
try context.save()
} catch {
print("Save error: \(error)")
}
}
}
}
Flights API Client
Network/FlightsAPIClient.swift:
import Foundation
protocol FlightsAPIClientProtocol {
func searchFlights(
from: String,
to: String,
departDate: Date
) async throws -> [Flight]
}
final class FlightsAPIClient: FlightsAPIClientProtocol {
private let baseURL = "https://travel.wildberries.ru/statistics/v1"
private let session: URLSession
init(session: URLSession = .shared) {
self.session = session
}
func searchFlights(
from: String,
to: String,
departDate: Date
) async throws -> [Flight] {
let dateFormatter = ISO8601DateFormatter()
let dateString = dateFormatter.string(from: departDate)
var components = URLComponents(
string: "\(baseURL)/cheap"
)
components?.queryItems = [
URLQueryItem(name: "origin", value: from),
URLQueryItem(name: "destination", value: to),
URLQueryItem(name: "departDate", value: dateString)
]
guard let url = components?.url else {
throw NetworkError.invalidURL
}
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let apiResponse = try decoder.decode(FlightsResponse.self, from: data)
return apiResponse.data.map { mapDTOToFlight($0) }
}
private func mapDTOToFlight(_ dto: FlightDTO) -> Flight {
let dateFormatter = ISO8601DateFormatter()
return Flight(
id: dto.id,
departureAirport: Airport(
code: dto.departure.code,
name: dto.departure.name,
city: dto.departure.city,
country: dto.departure.country
),
arrivalAirport: Airport(
code: dto.arrival.code,
name: dto.arrival.name,
city: dto.arrival.city,
country: dto.arrival.country
),
departureTime: dateFormatter.date(from: dto.departureDate) ?? Date(),
arrivalTime: dateFormatter.date(from: dto.arrivalDate) ?? Date(),
price: dto.price,
airline: dto.airline,
flightDuration: TimeInterval(dto.duration),
seats: dto.seats,
isFavorite: false
)
}
}
enum NetworkError: LocalizedError {
case invalidURL
case invalidResponse
case decodingError
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case .invalidResponse: return "Server error"
case .decodingError: return "Failed to parse data"
}
}
}
Favorites Service
Services/FavoritesService.swift:
import CoreData
protocol FavoritesServiceProtocol {
func addFavorite(_ flight: Flight) throws
func removeFavorite(_ flightId: String) throws
func isFavorite(_ flightId: String) -> Bool
func getAllFavorites() -> [Flight]
}
final class FavoritesService: FavoritesServiceProtocol {
private let coreDataStack = CoreDataStack.shared
func addFavorite(_ flight: Flight) throws {
let entity = FavoriteFlight(context: coreDataStack.viewContext)
entity.id = flight.id
entity.departureCity = flight.departureAirport.city
entity.arrivalCity = flight.arrivalAirport.city
entity.departureTime = flight.departureTime
entity.arrivalTime = flight.arrivalTime
entity.price = Int32(flight.price)
entity.airline = flight.airline
entity.addedDate = Date()
coreDataStack.save()
}
func removeFavorite(_ flightId: String) throws {
let request: NSFetchRequest<FavoriteFlight> = FavoriteFlight.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", flightId)
if let favorite = try coreDataStack.viewContext.fetch(request).first {
coreDataStack.viewContext.delete(favorite)
coreDataStack.save()
}
}
func isFavorite(_ flightId: String) -> Bool {
let request: NSFetchRequest<FavoriteFlight> = FavoriteFlight.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", flightId)
do {
return try coreDataStack.viewContext.fetch(request).count > 0
} catch {
return false
}
}
func getAllFavorites() -> [Flight] {
let request: NSFetchRequest<FavoriteFlight> = FavoriteFlight.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \\FavoriteFlight.addedDate, ascending: false)]
do {
let entities = try coreDataStack.viewContext.fetch(request)
return entities.map { entity in
Flight(
id: entity.id ?? "",
departureAirport: Airport(
code: "",
name: "",
city: entity.departureCity ?? "",
country: ""
),
arrivalAirport: Airport(
code: "",
name: "",
city: entity.arrivalCity ?? "",
country: ""
),
departureTime: entity.departureTime ?? Date(),
arrivalTime: entity.arrivalTime ?? Date(),
price: Int(entity.price),
airline: entity.airline ?? "",
flightDuration: 0,
seats: 0,
isFavorite: true
)
}
} catch {
return []
}
}
}
Flights List ViewModel
ViewModels/FlightsListViewModel.swift:
import Foundation
import Combine
@MainActor
final class FlightsListViewModel: ObservableObject {
@Published var flights: [Flight] = []
@Published var favoriteFlights: [Flight] = []
@Published var isLoading = false
@Published var errorMessage: String?
@Published var selectedTab: Tab = .all
@Published var departureCity: String = "SVO"
@Published var arrivalCity: String = "JFK"
@Published var departDate = Date().addingTimeInterval(86400) // Завтра
enum Tab {
case all
case favorites
}
private let apiClient: FlightsAPIClientProtocol
private let favoritesService: FavoritesServiceProtocol
init(
apiClient: FlightsAPIClientProtocol,
favoritesService: FavoritesServiceProtocol
) {
self.apiClient = apiClient
self.favoritesService = favoritesService
loadFavorites()
}
func searchFlights() {
isLoading = true
errorMessage = nil
Task {
do {
var flights = try await apiClient.searchFlights(
from: departureCity,
to: arrivalCity,
departDate: departDate
)
// Отмечаем избранные
flights = flights.map { flight in
var mutableFlight = flight
mutableFlight.isFavorite = favoritesService.isFavorite(flight.id)
return mutableFlight
}
self.flights = flights
self.isLoading = false
} catch {
self.errorMessage = error.localizedDescription
self.isLoading = false
}
}
}
func toggleFavorite(_ flight: Flight) {
Task {
do {
if favoritesService.isFavorite(flight.id) {
try favoritesService.removeFavorite(flight.id)
} else {
try favoritesService.addFavorite(flight)
}
loadFavorites()
searchFlights() // Перезагружаем текущий список
} catch {
errorMessage = error.localizedDescription
}
}
}
func loadFavorites() {
favoriteFlights = favoritesService.getAllFavorites()
}
}
Flights List View Controller
Views/FlightsListViewController.swift:
import UIKit
import Combine
final class FlightsListViewController: UIViewController {
private let viewModel: FlightsListViewModel
private let tableView = UITableView()
private let tabControl = UISegmentedControl(items: ["All Flights", "Favorites"])
private let loadingIndicator = UIActivityIndicatorView(style: .large)
private var cancellables = Set<AnyCancellable>()
init(viewModel: FlightsListViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Find Flights"
view.backgroundColor = .systemBackground
setupUI()
setupBindings()
}
private func setupUI() {
// Tab Control
tabControl.selectedSegmentIndex = 0
tabControl.addTarget(self, action: #selector(didChangeTab), for: .valueChanged)
tabControl.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tabControl)
// Search Section
let searchStack = createSearchStack()
searchStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(searchStack)
// Table View
tableView.dataSource = self
tableView.delegate = self
tableView.register(FlightCell.self, forCellReuseIdentifier: "flightCell")
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "loadingCell")
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
// Loading Indicator
loadingIndicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(loadingIndicator)
// Constraints
NSLayoutConstraint.activate([
tabControl.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
tabControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
tabControl.widthAnchor.constraint(equalToConstant: 250),
searchStack.topAnchor.constraint(equalTo: tabControl.bottomAnchor, constant: 12),
searchStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12),
searchStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12),
tableView.topAnchor.constraint(equalTo: searchStack.bottomAnchor, constant: 12),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
private func createSearchStack() -> UIStackView {
let fromField = UITextField()
fromField.placeholder = "From (SVO, JFK)"
fromField.borderStyle = .roundedRect
fromField.addTarget(self, action: #selector(updateFromCity), for: .editingChanged)
let toField = UITextField()
toField.placeholder = "To (JFK, CDG)"
toField.borderStyle = .roundedRect
toField.addTarget(self, action: #selector(updateToCity), for: .editingChanged)
let dateField = UITextField()
dateField.placeholder = "Date"
dateField.borderStyle = .roundedRect
let searchButton = UIButton(type: .system)
searchButton.setTitle("Search", for: .normal)
searchButton.backgroundColor = .systemBlue
searchButton.setTitleColor(.white, for: .normal)
searchButton.layer.cornerRadius = 8
searchButton.addTarget(self, action: #selector(didTapSearch), for: .touchUpInside)
let stack = UIStackView(arrangedSubviews: [fromField, toField, dateField, searchButton])
stack.axis = .vertical
stack.spacing = 8
return stack
}
private func setupBindings() {
viewModel.$flights
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.tableView.reloadData()
}
.store(in: &cancellables)
viewModel.$favoriteFlights
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.tableView.reloadData()
}
.store(in: &cancellables)
viewModel.$isLoading
.receive(on: DispatchQueue.main)
.sink { [weak self] isLoading in
if isLoading {
self?.loadingIndicator.startAnimating()
} else {
self?.loadingIndicator.stopAnimating()
}
}
.store(in: &cancellables)
viewModel.$errorMessage
.receive(on: DispatchQueue.main)
.sink { [weak self] error in
if let error = error {
self?.showError(error)
}
}
.store(in: &cancellables)
}
@objc private func didChangeTab() {
viewModel.selectedTab = tabControl.selectedSegmentIndex == 0 ? .all : .favorites
tableView.reloadData()
}
@objc private func updateFromCity(_ textField: UITextField) {
viewModel.departureCity = textField.text?.uppercased() ?? "SVO"
}
@objc private func updateToCity(_ textField: UITextField) {
viewModel.arrivalCity = textField.text?.uppercased() ?? "JFK"
}
@objc private func didTapSearch() {
viewModel.searchFlights()
}
private func showError(_ message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
extension FlightsListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch viewModel.selectedTab {
case .all:
return viewModel.flights.count
case .favorites:
return viewModel.favoriteFlights.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "flightCell", for: indexPath) as! FlightCell
let flight: Flight
switch viewModel.selectedTab {
case .all:
flight = viewModel.flights[indexPath.row]
case .favorites:
flight = viewModel.favoriteFlights[indexPath.row]
}
cell.configure(with: flight)
cell.onFavoriteTap = { [weak self] in
self?.viewModel.toggleFavorite(flight)
}
return cell
}
}
extension FlightsListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let flight: Flight
switch viewModel.selectedTab {
case .all:
flight = viewModel.flights[indexPath.row]
case .favorites:
flight = viewModel.favoriteFlights[indexPath.row]
}
let detailVM = FlightDetailViewModel(flight: flight)
let detailVC = FlightDetailViewController(viewModel: detailVM)
navigationController?.pushViewController(detailVC, animated: true)
tableView.deselectRow(at: indexPath, animated: true)
}
}
Flight Cell
Views/FlightCell.swift:
import UIKit
final class FlightCell: UITableViewCell {
private let departureLabel = UILabel()
private let arrivalLabel = UILabel()
private let timeLabel = UILabel()
private let priceLabel = UILabel()
private let favoriteButton = UIButton()
private let airlineLabel = UILabel()
var onFavoriteTap: (() -> Void)?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
// Container
let container = UIView()
container.layer.cornerRadius = 12
container.layer.borderWidth = 1
container.layer.borderColor = UIColor.systemGray5.cgColor
container.backgroundColor = .systemBackground
container.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(container)
// Routes
let routeStack = UIStackView()
routeStack.axis = .horizontal
routeStack.spacing = 12
routeStack.alignment = .center
departureLabel.font = .boldSystemFont(ofSize: 18)
arrivalLabel.font = .boldSystemFont(ofSize: 18)
let arrowLabel = UILabel()
arrowLabel.text = "→"
arrowLabel.font = .systemFont(ofSize: 16)
arrowLabel.textColor = .systemGray
routeStack.addArrangedSubview(departureLabel)
routeStack.addArrangedSubview(arrowLabel)
routeStack.addArrangedSubview(arrivalLabel)
// Time & Airline
timeLabel.font = .systemFont(ofSize: 12)
timeLabel.textColor = .systemGray
airlineLabel.font = .systemFont(ofSize: 12)
airlineLabel.textColor = .systemGray
// Price & Favorite
priceLabel.font = .boldSystemFont(ofSize: 16)
priceLabel.textColor = .systemBlue
favoriteButton.setImage(UIImage(systemName: "heart"), for: .normal)
favoriteButton.setImage(UIImage(systemName: "heart.fill"), for: .selected)
favoriteButton.tintColor = .systemRed
favoriteButton.addTarget(self, action: #selector(didTapFavorite), for: .touchUpInside)
// Layout
let vStack = UIStackView(arrangedSubviews: [routeStack, timeLabel, airlineLabel])
vStack.axis = .vertical
vStack.spacing = 4
vStack.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(vStack)
priceLabel.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(priceLabel)
favoriteButton.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(favoriteButton)
NSLayoutConstraint.activate([
container.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
container.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
container.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12),
container.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
vStack.topAnchor.constraint(equalTo: container.topAnchor, constant: 12),
vStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12),
vStack.trailingAnchor.constraint(lessThanOrEqualTo: priceLabel.leadingAnchor, constant: -12),
vStack.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -12),
priceLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor),
priceLabel.trailingAnchor.constraint(equalTo: favoriteButton.leadingAnchor, constant: -8),
favoriteButton.centerYAnchor.constraint(equalTo: container.centerYAnchor),
favoriteButton.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -12),
favoriteButton.widthAnchor.constraint(equalToConstant: 30),
favoriteButton.heightAnchor.constraint(equalToConstant: 30)
])
}
func configure(with flight: Flight) {
departureLabel.text = flight.departureAirport.code
arrivalLabel.text = flight.arrivalAirport.code
let timeFormatter = DateFormatter()
timeFormatter.timeStyle = .short
timeFormatter.dateStyle = .medium
let time = timeFormatter.string(from: flight.departureTime)
timeLabel.text = time
airlineLabel.text = flight.airline
priceLabel.text = "₽\(flight.price)"
favoriteButton.isSelected = flight.isFavorite
}
@objc private func didTapFavorite() {
onFavoriteTap?()
}
}
Flight Detail ViewController
Views/FlightDetailViewController.swift:
import UIKit
struct FlightDetailViewModel {
let flight: Flight
}
final class FlightDetailViewController: UIViewController {
private let viewModel: FlightDetailViewModel
private let scrollView = UIScrollView()
private let contentStack = UIStackView()
init(viewModel: FlightDetailViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Flight Details"
view.backgroundColor = .systemBackground
setupUI()
}
private func setupUI() {
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
contentStack.axis = .vertical
contentStack.spacing = 16
contentStack.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentStack)
NSLayoutConstraint.activate([
contentStack.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16),
contentStack.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16),
contentStack.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -16),
contentStack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -16),
contentStack.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -32)
])
addSection(title: "Route", content: "\(viewModel.flight.departureAirport.code) → \(viewModel.flight.arrivalAirport.code)")
addSection(title: "Departure", content: formatDate(viewModel.flight.departureTime))
addSection(title: "Arrival", content: formatDate(viewModel.flight.arrivalTime))
addSection(title: "Airline", content: viewModel.flight.airline)
addSection(title: "Price", content: "₽\(viewModel.flight.price)")
addSection(title: "Available Seats", content: "\(viewModel.flight.seats)")
}
private func addSection(title: String, content: String) {
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .boldSystemFont(ofSize: 14)
titleLabel.textColor = .systemGray
let contentLabel = UILabel()
contentLabel.text = content
contentLabel.font = .systemFont(ofSize: 16)
contentLabel.numberOfLines = 0
let stack = UIStackView(arrangedSubviews: [titleLabel, contentLabel])
stack.axis = .vertical
stack.spacing = 4
contentStack.addArrangedSubview(stack)
}
private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}
}
Ключевые особенности
- MVVM с Core Data — чистая архитектура
- Favorites Management — сохранение избранного локально
- API Integration — Wildberries travel API
- Двухэкранное приложение — список и детали
- Tab Control — переключение между всеми и избранными
- Визуальное выделение — сердечко для избранного
- Поиск рейсов — по городам и дате
- Обработка ошибок — правильное показание ошибок
Это production-ready приложение для поиска авиаперелётов.