Оптимизация экрана загрузки списка пользователей

Здесь loading выключается до завершения callback-запросов, names меняется с фоновой очереди, модель имеет неверное владение, а одинаковые имена дают конфликтующие ID списка.

Короткий ответ

Что ответить на собеседовании

Здесь loading выключается до завершения callback-запросов, names меняется с фоновой очереди, модель имеет неверное владение, а одинаковые имена дают конфликтующие ID списка.

Подробный разбор

Ответ с пояснениями

Условие

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

Требуется изучить текущую реализацию и исправить все найденные недочёты.

struct ContentView: View {
    @ObservedObject private var vm = ViewModel()
    @State private var userCount = 3.0

    var body: some View {
        ZStack(alignment: .top) {
            Background(count: $userCount)
                .frame(width: UIScreen.main.bounds.width, height: 115)

            VStack {
                Slider(
                    value: $userCount,
                    in: 3...100,
                    step: 1
                ) { _ in }
                .tint(.purple)

                Button("Запросить \(Int(userCount)) пользователей") {
                    Task {
                        await vm.downloadNames(count: Int(userCount))
                    }
                }
                .padding()
                .foregroundStyle(.white)
                .background(.purple)
                .clipShape(.buttonBorder)

                ProxyView(viewModel: vm)
            }
        }
    }
}

struct Background: View {
    @Binding var count: Double
    var backgroundColor: Color {
        switch count {
        case ..<10: return .green
        case 10..<50: return .yellow
        default: return .red
        }
    }

    var body: some View {
        Rectangle()
            .fill(backgroundColor)
    }
}

struct ProxyView: View {
    @ObservedObject var viewModel: ViewModel
    var body: some View {
        SuccessView(viewModel: viewModel)
    }
}

struct SuccessView: View {
    @ObservedObject var viewModel: ViewModel
    @State private var loading: Bool = false

    var body: some View {
        VStack {
            Text("Total users fetched: \(viewModel.names.count)")
            
            if loading { ProgressView() }

            List(viewModel.names, id: \ .self) { name in
                Text(name)
            }
        }
        .onReceive(viewModel.publisher) { value in
            loading = value
        }
    }
}

class ViewModel: ObservableObject {
    let networkService = NetworkService()
    let value = CurrentValueSubject<Bool, Never>(false)

    @Published var names: [String] = []
    var publisher: AnyPublisher<Bool, Never> {
        value.eraseToAnyPublisher()
    }

    func downloadNames(count: Int) async {
        value.send(true)
        async let ids = networkService.fetchUserIds(count: count)

        for id in await ids {
            networkService.fetchName(id: id) { name in
                self.names.append(name)
            }
        }

        value.send(false)
    }
}

class NetworkService {
    func fetchName(id: Int, completion: @escaping (String) -> Void) {
        let names = ["Александр", "Максим", "Иван", "Артём", "Дмитрий", "Никито", "Михаил", "Даниил", "Егор"]
        let surnames = ["Смирное", "Иванов", "Кузнецов", "Соколов", "Попов", "Лебедев", "Козлов", "Новиков", "Морозов"]

        DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 1) {
            completion(names.randomElement()! + " " + surnames.randomElement()!)
        }
    }

    func fetchUserIds(count: Int) async -> [Int] {
        try? await Task.sleep(for: .seconds(1))
        return Array(1...count)
    }
}

# Preview {
    ContentView()
}

Ответ

Здесь loading выключается до завершения callback-запросов, names меняется с фоновой очереди, модель имеет неверное владение, а одинаковые имена дают конфликтующие ID списка.

Исправления по слоям:

  • В ContentView используйте @StateObject для созданной здесь модели, в дочерних — @ObservedObject.
  • Пометьте ViewModel @MainActor; храните опубликованные isLoading, error и пользователей с серверным id, а не только имена.
  • Преобразуйте fetchName в async throws и действительно дождитесь всех результатов, например task group с ограничением шести одновременных запросов. Собирайте результат локально и публикуйте одним присваиванием, сохранив порядок ID.
  • Сбрасывайте loading через defer после ожидания; отмену не подавляйте try?. При новом запросе отменяйте прежний и проверяйте generation перед записью результата либо блокируйте повторный запуск.
  • Уберите отдельный CurrentValueSubject и копию loading в SuccessView. Используйте List(users), где User: Identifiable, исправив ошибочный key path \ .self.
  • Background достаточно обычного count: он не изменяет binding. Вместо UIScreen.main.bounds используйте ширину контейнера; полную пагинацию условие не требует.

Для callback-адаптера continuation должна завершаться ровно один раз; отмена требует связи с реальной сетевой задачей, а не только проверки после ответа.

ИИ-помощник для собеседований

Хочешь уверенно проходить собеседования?

Попробуй ИИ-помощник для собеседований: слышит вас и собеседника, анализирует экран, подсказывает ответы в реальном времени, работает без VPN и не попадает в захват экрана.

Подробнее