78 lines
2.5 KiB
Swift
78 lines
2.5 KiB
Swift
//
|
|
// ProjectListModel.swift
|
|
// Snail
|
|
//
|
|
|
|
import Foundation
|
|
import Observation
|
|
|
|
/// Постраничная загрузка списка проектов: следующая страница подгружается при прокрутке.
|
|
@Observable
|
|
final class ProjectListModel {
|
|
private static let pageSize = 20
|
|
|
|
private(set) var projects: [Project] = []
|
|
private(set) var isLoading = false
|
|
private(set) var hasLoaded = false
|
|
private(set) var error: APIError?
|
|
|
|
private var loadedPage = 0
|
|
private var totalPages = 1
|
|
private let service: ProjectService
|
|
|
|
init(service: ProjectService = ProjectService()) {
|
|
self.service = service
|
|
}
|
|
|
|
var hasMorePages: Bool {
|
|
loadedPage < totalPages
|
|
}
|
|
|
|
/// Загружает первую страницу заново (первый показ, pull-to-refresh, «Повторить»).
|
|
func reload() async {
|
|
await load(page: 1)
|
|
}
|
|
|
|
/// Вызывается при появлении строки: если это последняя строка, грузим следующую страницу.
|
|
func loadMoreIfNeeded(after project: Project) async {
|
|
guard project.id == projects.last?.id, hasMorePages, !isLoading else { return }
|
|
await load(page: loadedPage + 1)
|
|
}
|
|
|
|
func create(key: String, name: String) async throws {
|
|
let project = try await service.create(key: key, name: name)
|
|
projects.insert(project, at: 0)
|
|
}
|
|
|
|
func rename(_ project: Project, to name: String) async throws {
|
|
let updated = try await service.update(id: project.id, name: name)
|
|
if let index = projects.firstIndex(where: { $0.id == updated.id }) {
|
|
projects[index] = updated
|
|
}
|
|
}
|
|
|
|
private func load(page: Int) async {
|
|
isLoading = true
|
|
defer { isLoading = false }
|
|
|
|
do {
|
|
let result = try await service.list(page: page, limit: Self.pageSize)
|
|
if page == 1 {
|
|
projects = result.list
|
|
} else {
|
|
// Список мог сдвинуться между запросами — отбрасываем дубликаты.
|
|
let known = Set(projects.map(\.id))
|
|
projects += result.list.filter { !known.contains($0.id) }
|
|
}
|
|
loadedPage = page
|
|
totalPages = result.meta.totalPages
|
|
error = nil
|
|
} catch let apiError as APIError {
|
|
error = apiError
|
|
} catch {
|
|
self.error = .invalidResponse
|
|
}
|
|
hasLoaded = true
|
|
}
|
|
}
|