Sobes.tech
Junior — Senior

Optimization of the user list loading screen

livecode

Task condition

The module is implemented as a screen with a slider to select the number of requested users, a button to initiate the download, and several views to display the result. The full list is displayed entirely, without pagination.

It is required to study the current implementation and fix all identified issues.

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("Request \(Int(userCount)) users") {
                    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 = ["Alexander", "Maxim", "Ivan", "Artyom", "Dmitry", "Nikito", "Mikhail", "Daniil", "Egor"]
        let surnames = ["Smirnov", "Ivanov", "Kuznetsov", "Sokolov", "Popov", "Lebedev", "Kozlov", "Novikov", "Morozov"]

        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()
}