Added infinite scrolling to BookmarksView with dynamic loading of bookmarks on

This commit is contained in:
Ilyas Hallak 2025-06-25 22:39:02 +02:00
parent 789d581705
commit 601f81dc9f
7 changed files with 78 additions and 13 deletions

View File

@ -174,7 +174,10 @@
}, },
"subpath" : "Yams" "subpath" : "Yams"
} }
],
"prebuilts" : [
] ]
}, },
"version" : 6 "version" : 7
} }

View File

@ -10,7 +10,7 @@ import Foundation
protocol PAPI { protocol PAPI {
var tokenProvider: TokenProvider { get } var tokenProvider: TokenProvider { get }
func login(username: String, password: String) async throws -> UserDto func login(username: String, password: String) async throws -> UserDto
func getBookmarks(state: BookmarkState?) async throws -> [BookmarkDto] func getBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?) async throws -> [BookmarkDto]
func getBookmark(id: String) async throws -> BookmarkDetailDto func getBookmark(id: String) async throws -> BookmarkDetailDto
func getBookmarkArticle(id: String) async throws -> String func getBookmarkArticle(id: String) async throws -> String
func createBookmark(createRequest: CreateBookmarkRequestDto) async throws -> CreateBookmarkResponseDto func createBookmark(createRequest: CreateBookmarkRequestDto) async throws -> CreateBookmarkResponseDto
@ -131,21 +131,39 @@ class API: PAPI {
return userDto return userDto
} }
func getBookmarks(state: BookmarkState? = nil) async throws -> [BookmarkDto] { func getBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil) async throws -> [BookmarkDto] {
var endpoint = "/api/bookmarks" var endpoint = "/api/bookmarks"
var queryItems: [URLQueryItem] = []
// Query-Parameter basierend auf State hinzufügen // Query-Parameter basierend auf State hinzufügen
if let state = state { if let state = state {
switch state { switch state {
case .unread: case .unread:
endpoint += "?is_archived=false&is_marked=false" queryItems.append(URLQueryItem(name: "is_archived", value: "false"))
queryItems.append(URLQueryItem(name: "is_marked", value: "false"))
case .favorite: case .favorite:
endpoint += "?is_marked=true" queryItems.append(URLQueryItem(name: "is_marked", value: "true"))
case .archived: case .archived:
endpoint += "?is_archived=true" queryItems.append(URLQueryItem(name: "is_archived", value: "true"))
} }
} }
if let limit = limit {
queryItems.append(URLQueryItem(name: "limit", value: "\(limit)"))
}
if let offset = offset {
queryItems.append(URLQueryItem(name: "offset", value: "\(offset)"))
}
if let search = search {
queryItems.append(URLQueryItem(name: "search", value: search))
}
if !queryItems.isEmpty {
let queryString = queryItems.map { "\($0.name)=\($0.value ?? "")" }.joined(separator: "&")
endpoint += "?\(queryString)"
}
return try await makeJSONRequest( return try await makeJSONRequest(
endpoint: endpoint, endpoint: endpoint,
responseType: [BookmarkDto].self responseType: [BookmarkDto].self

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
protocol PBookmarksRepository { protocol PBookmarksRepository {
func fetchBookmarks(state: BookmarkState?) async throws -> [Bookmark] func fetchBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?) async throws -> [Bookmark]
func fetchBookmark(id: String) async throws -> BookmarkDetail func fetchBookmark(id: String) async throws -> BookmarkDetail
func fetchBookmarkArticle(id: String) async throws -> String func fetchBookmarkArticle(id: String) async throws -> String
func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String
@ -16,8 +16,8 @@ class BookmarksRepository: PBookmarksRepository {
self.api = api self.api = api
} }
func fetchBookmarks(state: BookmarkState? = nil) async throws -> [Bookmark] { func fetchBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil) async throws -> [Bookmark] {
let bookmarkDtos = try await api.getBookmarks(state: state) let bookmarkDtos = try await api.getBookmarks(state: state, limit: limit, offset: offset, search: search)
return bookmarkDtos.map { $0.toDomain() } return bookmarkDtos.map { $0.toDomain() }
} }

View File

@ -7,8 +7,8 @@ class GetBookmarksUseCase {
self.repository = repository self.repository = repository
} }
func execute(state: BookmarkState? = nil) async throws -> [Bookmark] { func execute(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil) async throws -> [Bookmark] {
let allBookmarks = try await repository.fetchBookmarks(state: state) let allBookmarks = try await repository.fetchBookmarks(state: state, limit: limit, offset: offset, search: search)
// Fallback-Filterung auf Client-Seite falls API keine Query-Parameter unterstützt // Fallback-Filterung auf Client-Seite falls API keine Query-Parameter unterstützt
if let state = state { if let state = state {

View File

@ -40,6 +40,13 @@ struct BookmarksView: View {
} }
} }
) )
.onAppear {
if bookmark.id == viewModel.bookmarks.last?.id {
Task {
await viewModel.loadMoreBookmarks()
}
}
}
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
@ -60,6 +67,7 @@ struct BookmarksView: View {
) )
} }
} }
} }
// FAB Button - nur bei "Ungelesen" anzeigen // FAB Button - nur bei "Ungelesen" anzeigen

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import Combine import Combine
import SwiftUI
@Observable @Observable
class BookmarksViewModel { class BookmarksViewModel {
@ -18,6 +19,11 @@ class BookmarksViewModel {
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
// Pagination-Variablen
private var limit = 20
private var offset = 0
private var hasMoreData = true
init() { init() {
setupNotificationObserver() setupNotificationObserver()
} }
@ -52,9 +58,13 @@ class BookmarksViewModel {
isLoading = true isLoading = true
errorMessage = nil errorMessage = nil
currentState = state currentState = state
offset = 0 // Offset zurücksetzen
hasMoreData = true // Pagination zurücksetzen
do { do {
bookmarks = try await getBooksmarksUseCase.execute(state: state) let newBookmarks = try await getBooksmarksUseCase.execute(state: state, limit: limit, offset: offset)
bookmarks = newBookmarks
hasMoreData = newBookmarks.count == limit // Prüfen, ob weitere Daten verfügbar sind
} catch { } catch {
errorMessage = "Fehler beim Laden der Bookmarks" errorMessage = "Fehler beim Laden der Bookmarks"
bookmarks = [] bookmarks = []
@ -63,6 +73,25 @@ class BookmarksViewModel {
isLoading = false isLoading = false
} }
@MainActor
func loadMoreBookmarks() async {
guard !isLoading && hasMoreData else { return } // Verhindern, dass mehrfach geladen wird
isLoading = true
errorMessage = nil
do {
offset += limit // Offset erhöhen
let newBookmarks = try await getBooksmarksUseCase.execute(state: currentState, limit: limit, offset: offset)
bookmarks.append(contentsOf: newBookmarks) // Neue Bookmarks hinzufügen
hasMoreData = newBookmarks.count == limit // Prüfen, ob weitere Daten verfügbar sind
} catch {
errorMessage = "Fehler beim Nachladen der Bookmarks"
}
isLoading = false
}
@MainActor @MainActor
func refreshBookmarks() async { func refreshBookmarks() async {
await loadBookmarks(state: currentState) await loadBookmarks(state: currentState)

View File

@ -0,0 +1,7 @@
//
// File.swift
// readeck
//
// Created by Ilyas Hallak on 25.06.25.
//