Здесь 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 должна завершаться ровно один раз; отмена требует связи с реальной сетевой задачей, а не только проверки после ответа.