← Назад к вопросам

Как на прошлом проекте происходила передача данных с сети на UI?

2.0 Middle🔥 221 комментариев
#Архитектура и паттерны#Работа с сетью

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Архитектура передачи данных от сети к UI на проекте

На последнем проекте мы использовали многоуровневую архитектуру (Layered Architecture) с четким разделением ответственности. Основной подход — реактивное программирование с Combine для обеспечения однонаправленного потока данных и автоматического обновления UI.

Ключевые компоненты архитектуры:

  1. Сетевой слой (Network Layer)
  2. Слой репозиториев (Repository Layer)
  3. Слой бизнес-логики (Business Logic Layer)
  4. Презентационный слой (Presentation Layer)

Детальный процесс передачи данных:

1. Сетевой слой и сериализация

protocol NetworkServiceProtocol {
    func fetch<T: Decodable>(_ endpoint: Endpoint) -> AnyPublisher<T, NetworkError>
}

class NetworkService: NetworkServiceProtocol {
    private let session: URLSession
    private let decoder: JSONDecoder
    
    func fetch<T: Decodable>(_ endpoint: Endpoint) -> AnyPublisher<T, NetworkError> {
        return session.dataTaskPublisher(for: endpoint.urlRequest)
            .mapError { NetworkError.transportError($0) }
            .tryMap { data, response in
                guard let httpResponse = response as? HTTPURLResponse else {
                    throw NetworkError.invalidResponse
                }
                guard 200...299 ~= httpResponse.statusCode else {
                    throw NetworkError.serverError(statusCode: httpResponse.statusCode)
                }
                return data
            }
            .decode(type: T.self, decoder: decoder)
            .mapError { error in
                if let decodingError = error as? DecodingError {
                    return NetworkError.decodingError(decodingError)
                }
                return NetworkError.unknown(error)
            }
            .eraseToAnyPublisher()
    }
}

2. Репозитории как абстракция над источниками данных

protocol UserRepositoryProtocol {
    func fetchUsers() -> AnyPublisher<[User], Error>
    func fetchUserDetails(id: String) -> AnyPublisher<UserDetail, Error>
}

class UserRepository: UserRepositoryProtocol {
    private let networkService: NetworkServiceProtocol
    private let cache: UserCacheProtocol
    
    func fetchUsers() -> AnyPublisher<[User], Error> {
        // Проверяем кэш, затем сеть
        return cache.getUsers()
            .catch { _ in
                self.networkService.fetch(UsersEndpoint())
                    .handleEvents(receiveOutput: { [weak self] users in
                        self?.cache.saveUsers(users)
                    })
            }
            .eraseToAnyPublisher()
    }
}

3. ViewModel с Combine для управления состоянием

class UserListViewModel: ObservableObject {
    @Published private(set) var users: [User] = []
    @Published private(set) var isLoading = false
    @Published private(set) var error: Error?
    
    private let repository: UserRepositoryProtocol
    private var cancellables = Set<AnyCancellable>()
    
    init(repository: UserRepositoryProtocol) {
        self.repository = repository
    }
    
    func loadUsers() {
        isLoading = true
        error = nil
        
        repository.fetchUsers()
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { [weak self] completion in
                    self?.isLoading = false
                    if case .failure(let error) = completion {
                        self?.error = error
                    }
                },
                receiveValue: { [weak self] users in
                    self?.users = users
                }
            )
            .store(in: &cancellables)
    }
}

4. SwiftUI View с реактивными биндингами

struct UserListView: View {
    @StateObject private var viewModel: UserListViewModel
    
    var body: some View {
        NavigationView {
            ZStack {
                List(viewModel.users) { user in
                    UserRow(user: user)
                }
                .navigationTitle("Users")
                .refreshable {
                    viewModel.loadUsers()
                }
                
                if viewModel.isLoading {
                    ProgressView()
                        .scaleEffect(1.5)
                }
                
                if let error = viewModel.error {
                    ErrorView(error: error, retryAction: {
                        viewModel.loadUsers()
                    })
                }
            }
        }
        .onAppear {
            if viewModel.users.isEmpty {
                viewModel.loadUsers()
            }
        }
    }
}

Ключевые принципы реализации:

Разделение ответственности: Каждый слой выполняет строго определенные задачи:

  • Сетевой слой: HTTP-запросы и базовая обработка ошибок
  • Репозитории: абстракция над источниками данных (сеть, кэш, база данных)
  • ViewModel: подготовка данных для UI, бизнес-логика
  • View: отображение данных и обработка пользовательских действий

Реактивность: Использование Combine обеспечивает:

  • Автоматическое обновление UI при изменении данных
  • Отмену асинхронных операций при деинициализации
  • Композицию и трансформацию потоков данных

Обработка ошибок: Многоуровневая обработка:

  1. Сетевые ошибки (таймауты, статус коды)
  2. Ошибки парсинга JSON
  3. Ошибки бизнес-логики
  4. Пользовательский интерфейс для отображения ошибок

Кэширование: Стратегия "кэш-потом-сеть" (Cache-Then-Network):

  • Быстрое отображение данных из кэша
  • Фоновая загрузка актуальных данных
  • Бесшовное обновление UI

Тестируемость: Каждый слой изолирован и тестируется отдельно:

  • Моки для сетевого слоя
  • Юнит-тесты для ViewModel
  • UI-тесты для SwiftUI компонентов

Такой подход обеспечил стабильность, масштабируемость и поддерживаемость кода, позволяя легко добавлять новые фичи и модифицировать существующие без нарушения работы всей системы.