This commit is contained in:
vadim committed 2026-10-29 13:12:45 +03:00
1 parent 558bb005f4
commit 9ed2f17768
14 files changed
+1207 -94

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
//
// 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
}
}