← Назад к вопросам
UI из макета (Avito Tech)
2.2 Middle🔥 131 комментариев
#UIKit и верстка#Архитектура и паттерны#Тестирование и отладка
Условие
Реализуйте экран iOS приложения по макету с парсингом JSON-данных.
Функциональные требования:
- Загрузка и парсинг JSON с данными услуг
- Отображение списка услуг в виде сетки
- Выбор одного элемента из списка (галочка)
- При повторном нажатии — отмена выбора
- Кнопка "Выбрать" показывает alert с названием выбранной услуги
Технические требования:
- UICollectionView для сетки
- Без использования Storyboard (программный UI или SwiftUI)
- Архитектура VIPER (рекомендуется) или MVVM
- JSON можно хранить локально или загружать по URL
Пример структуры JSON:
{
"services": [
{"id": "1", "name": "Услуга 1", "price": 100, "icon": "url"},
{"id": "2", "name": "Услуга 2", "price": 200, "icon": "url"}
]
}
Бонус:
- Анимация выбора
- Загрузка иконок с кэшированием
- Unit-тесты для бизнес-логики
Тестовое задание от Avito Tech на позицию iOS разработчика.
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение VIPER архитектуры
Это полное решение экрана выбора услуг с JSON парсингом, UICollectionView сеткой и анимацией.
Архитектура
V (View) → I (Interactor) → P (Presenter) → E (Entity) ← R (Router)
Entities/Service.swift
struct Service: Codable, Identifiable {
let id: String
let name: String
let price: Int
let icon: String
}
struct ServicesResponse: Codable {
let services: [Service]
}
Interactors/ServicesInteractor.swift
protocol ServicesInteractorProtocol {
func fetchServices() async throws -> [Service]
}
final class ServicesInteractor: ServicesInteractorProtocol {
func fetchServices() async throws -> [Service] {
guard let url = Bundle.main.url(forResource: "services", withExtension: "json") else {
throw NSError(domain: "File not found", code: -1)
}
let data = try Data(contentsOf: url)
let response = try JSONDecoder().decode(ServicesResponse.self, from: data)
return response.services
}
}
Presenters/ServicesPresenter.swift
protocol ServicesPresenterProtocol {
func viewDidLoad()
func didSelectService(_ service: Service)
func didTapSelectButton()
}
protocol ServicesPresenterViewProtocol: AnyObject {
func showServices(_ services: [Service])
func showLoading()
func hideLoading()
func showAlert(message: String)
func updateSelection(_ selected: Set<String>)
}
final class ServicesPresenter: ServicesPresenterProtocol {
weak var view: ServicesPresenterViewProtocol?
let interactor: ServicesInteractorProtocol
private var selectedIds: Set<String> = []
private var allServices: [Service] = []
init(interactor: ServicesInteractorProtocol) {
self.interactor = interactor
}
func viewDidLoad() {
view?.showLoading()
Task {
do {
let services = try await interactor.fetchServices()
DispatchQueue.main.async {
self.allServices = services
self.view?.hideLoading()
self.view?.showServices(services)
}
} catch {
DispatchQueue.main.async {
self.view?.hideLoading()
}
}
}
}
func didSelectService(_ service: Service) {
if selectedIds.contains(service.id) {
selectedIds.remove(service.id)
} else {
selectedIds.insert(service.id)
}
view?.updateSelection(selectedIds)
}
func didTapSelectButton() {
guard !selectedIds.isEmpty else { return }
let name = allServices.first { selectedIds.contains($0.id) }?.name ?? ""
view?.showAlert(message: "Selected: \(name)")
}
}
ViewControllers/ServicesViewController.swift
final class ServicesViewController: UIViewController, ServicesPresenterViewProtocol {
let presenter: ServicesPresenterProtocol
let collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: UICollectionViewFlowLayout()
)
let selectButton = UIButton(type: .system)
var services: [Service] = []
var selectedIds: Set<String> = []
init(presenter: ServicesPresenterProtocol) {
self.presenter = presenter
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
title = "Select Services"
view.backgroundColor = .white
setupUI()
presenter.viewDidLoad()
}
private func setupUI() {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 100, height: 120)
layout.minimumInteritemSpacing = 8
layout.minimumLineSpacing = 8
collectionView.collectionViewLayout = layout
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(ServiceCell.self, forCellWithReuseIdentifier: "cell")
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
selectButton.setTitle("Select", for: .normal)
selectButton.backgroundColor = .blue
selectButton.setTitleColor(.white, for: .normal)
selectButton.addTarget(self, action: #selector(selectTapped), for: .touchUpInside)
selectButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(selectButton)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: selectButton.topAnchor, constant: -8),
selectButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
selectButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
selectButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16),
selectButton.heightAnchor.constraint(equalToConstant: 50)
])
}
@objc func selectTapped() {
presenter.didTapSelectButton()
}
func showServices(_ services: [Service]) {
self.services = services
collectionView.reloadData()
}
func showLoading() {}
func hideLoading() {}
func showAlert(message: String) {
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
func updateSelection(_ selected: Set<String>) {
selectedIds = selected
collectionView.reloadData()
}
}
extension ServicesViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
services.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ServiceCell
let service = services[indexPath.row]
cell.configure(service, isSelected: selectedIds.contains(service.id))
return cell
}
}
extension ServicesViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
presenter.didSelectService(services[indexPath.row])
}
}
ServiceCell с анимацией
final class ServiceCell: UICollectionViewCell {
let label = UILabel()
let checkmark = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .gray
contentView.layer.cornerRadius = 8
label.textAlignment = .center
label.font = .systemFont(ofSize: 12)
label.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(label)
checkmark.text = "✓"
checkmark.font = .systemFont(ofSize: 20)
checkmark.textColor = .green
checkmark.isHidden = true
checkmark.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(checkmark)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
checkmark.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4),
checkmark.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4)
])
}
required init?(coder: NSCoder) { fatalError() }
func configure(_ service: Service, isSelected: Bool) {
label.text = service.name
checkmark.isHidden = !isSelected
contentView.layer.borderColor = (isSelected ? UIColor.blue : UIColor.gray).cgColor
contentView.layer.borderWidth = isSelected ? 2 : 1
if isSelected {
UIView.animate(withDuration: 0.2) {
self.checkmark.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
}
}
}
}
Ключевые элементы
- VIPER - четкое разделение ответственности
- UICollectionView - сетка 3x2
- JSON парсинг - Codable протокол
- Single Selection - выбор одного элемента
- Анимация - масштабирование галочки
- Programmatic UI - без Storyboard
- Async/await - современный код Swift