mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-02-02 11:12:20 +00:00
Refactor to NextPageView + handle next page loading failure
This commit is contained in:
parent
0739264005
commit
219703ecc7
11 changed files with 187 additions and 215 deletions
|
@ -191,17 +191,11 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNextPage() async {
|
func fetchNextPage() async throws {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
|
||||||
switch selectedTab {
|
switch selectedTab {
|
||||||
case .statuses, .replies, .boosts, .media:
|
case .statuses, .replies, .boosts, .media:
|
||||||
guard let lastId = statuses.last?.id else { return }
|
guard let lastId = statuses.last?.id else { return }
|
||||||
if selectedTab == .boosts {
|
|
||||||
statusesState = .display(statuses: boosts, nextPageState: .loadingNextPage)
|
|
||||||
} else {
|
|
||||||
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)
|
|
||||||
}
|
|
||||||
let newStatuses: [Status] =
|
let newStatuses: [Status] =
|
||||||
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
||||||
sinceId: lastId,
|
sinceId: lastId,
|
||||||
|
@ -238,9 +232,6 @@ import SwiftUI
|
||||||
bookmarks.append(contentsOf: newBookmarks)
|
bookmarks.append(contentsOf: newBookmarks)
|
||||||
statusesState = .display(statuses: bookmarks, nextPageState: .hasNextPage)
|
statusesState = .display(statuses: bookmarks, nextPageState: .hasNextPage)
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
statusesState = .error(error: error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reloadTabState() {
|
private func reloadTabState() {
|
||||||
|
|
|
@ -134,21 +134,13 @@ public struct AccountsListView: View {
|
||||||
|
|
||||||
switch nextPageState {
|
switch nextPageState {
|
||||||
case .hasNextPage:
|
case .hasNextPage:
|
||||||
loadingRow
|
NextPageView {
|
||||||
|
try await viewModel.fetchNextPage()
|
||||||
|
}
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.onAppear {
|
|
||||||
Task {
|
|
||||||
await viewModel.fetchNextPage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case .loadingNextPage:
|
|
||||||
loadingRow
|
|
||||||
#if !os(visionOS)
|
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
|
||||||
#endif
|
|
||||||
case .none:
|
case .none:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
@ -160,12 +152,4 @@ public struct AccountsListView: View {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingRow: some View {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
ProgressView()
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ public enum AccountsListMode {
|
||||||
|
|
||||||
public enum State {
|
public enum State {
|
||||||
public enum PagingState {
|
public enum PagingState {
|
||||||
case hasNextPage, loadingNextPage, none
|
case hasNextPage, none
|
||||||
}
|
}
|
||||||
|
|
||||||
case loading
|
case loading
|
||||||
|
@ -94,10 +94,8 @@ public enum AccountsListMode {
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNextPage() async {
|
func fetchNextPage() async throws {
|
||||||
guard let client, let nextPageId else { return }
|
guard let client, let nextPageId else { return }
|
||||||
do {
|
|
||||||
state = .display(accounts: accounts, relationships: relationships, nextPageState: .loadingNextPage)
|
|
||||||
let newAccounts: [Account]
|
let newAccounts: [Account]
|
||||||
let link: LinkHandler?
|
let link: LinkHandler?
|
||||||
switch mode {
|
switch mode {
|
||||||
|
@ -126,10 +124,6 @@ public enum AccountsListMode {
|
||||||
state = .display(accounts: accounts,
|
state = .display(accounts: accounts,
|
||||||
relationships: relationships,
|
relationships: relationships,
|
||||||
nextPageState: link?.maxId != nil ? .hasNextPage : .none)
|
nextPageState: link?.maxId != nil ? .hasNextPage : .none)
|
||||||
} catch {
|
|
||||||
let logger = Logger(subsystem: "com.icecubesapp", category: "UI")
|
|
||||||
logger.log(level: .info, "\(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func search() async {
|
func search() async {
|
||||||
|
|
|
@ -53,18 +53,14 @@ public class AccountStatusesListViewModel: StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchNextPage() async {
|
public func fetchNextPage() async throws {
|
||||||
guard let client, let nextId = nextPage?.maxId else { return }
|
guard let client, let nextId = nextPage?.maxId else { return }
|
||||||
statusesState = .display(statuses: statuses,
|
|
||||||
nextPageState: .loadingNextPage)
|
|
||||||
do {
|
|
||||||
var newStatuses: [Status] = []
|
var newStatuses: [Status] = []
|
||||||
(newStatuses, nextPage) = try await client.getWithLink(endpoint: mode.endpoint(sinceId: nextId))
|
(newStatuses, nextPage) = try await client.getWithLink(endpoint: mode.endpoint(sinceId: nextId))
|
||||||
statuses.append(contentsOf: newStatuses)
|
statuses.append(contentsOf: newStatuses)
|
||||||
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
||||||
statusesState = .display(statuses: statuses,
|
statusesState = .display(statuses: statuses,
|
||||||
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
|
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
|
||||||
} catch { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func statusDidAppear(status: Status) {
|
public func statusDidAppear(status: Status) {
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct NextPageView: View {
|
||||||
|
@State private var isLoadingNextPage: Bool = false
|
||||||
|
@State private var showRetry: Bool = false
|
||||||
|
|
||||||
|
let loadNextPage: (() async throws -> Void)
|
||||||
|
|
||||||
|
public init(loadNextPage: @escaping (() async throws -> Void)) {
|
||||||
|
self.loadNextPage = loadNextPage
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
if showRetry {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
showRetry = false
|
||||||
|
await executeTask()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("action.retry", systemImage: "arrow.clockwise")
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
} else {
|
||||||
|
Label("placeholder.loading.short", systemImage: "arrow.down")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.symbolEffect(.pulse, value: isLoadingNextPage)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
await executeTask()
|
||||||
|
}
|
||||||
|
.listRowSeparator(.hidden, edges: .all)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func executeTask() async {
|
||||||
|
showRetry = false
|
||||||
|
defer {
|
||||||
|
isLoadingNextPage = false
|
||||||
|
}
|
||||||
|
guard !isLoadingNextPage else { return }
|
||||||
|
isLoadingNextPage = true
|
||||||
|
do {
|
||||||
|
try await loadNextPage()
|
||||||
|
} catch {
|
||||||
|
showRetry = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,25 +25,13 @@ public struct TrendingLinksListView: View {
|
||||||
#endif
|
#endif
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
HStack {
|
NextPageView {
|
||||||
Spacer()
|
let nextPage: [Card] = try await client.get(endpoint: Trends.links(offset: links.count))
|
||||||
ProgressView()
|
links.append(contentsOf: nextPage)
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.task {
|
|
||||||
defer {
|
|
||||||
isLoadingNextPage = false
|
|
||||||
}
|
|
||||||
guard !isLoadingNextPage else { return }
|
|
||||||
isLoadingNextPage = true
|
|
||||||
do {
|
|
||||||
let nextPage: [Card] = try await client.get(endpoint: Trends.links(offset: links.count))
|
|
||||||
links.append(contentsOf: nextPage)
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
|
|
|
@ -180,20 +180,22 @@ public struct NotificationsListView: View {
|
||||||
#endif
|
#endif
|
||||||
.id(notification.id)
|
.id(notification.id)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
switch nextPageState {
|
switch nextPageState {
|
||||||
case .none:
|
case .none:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
case .hasNextPage:
|
case .hasNextPage:
|
||||||
loadingRow
|
NextPageView {
|
||||||
.onAppear {
|
try await viewModel.fetchNextPage()
|
||||||
Task {
|
|
||||||
await viewModel.fetchNextPage()
|
|
||||||
}
|
}
|
||||||
|
.listRowInsets(.init(top: .layoutPadding,
|
||||||
|
leading: .layoutPadding + 4,
|
||||||
|
bottom: .layoutPadding,
|
||||||
|
trailing: .layoutPadding))
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
case .loadingNextPage:
|
|
||||||
loadingRow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .error:
|
case .error:
|
||||||
|
@ -212,21 +214,6 @@ public struct NotificationsListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingRow: some View {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
ProgressView()
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.listRowInsets(.init(top: .layoutPadding,
|
|
||||||
leading: .layoutPadding + 4,
|
|
||||||
bottom: .layoutPadding,
|
|
||||||
trailing: .layoutPadding))
|
|
||||||
#if !os(visionOS)
|
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private var topPaddingView: some View {
|
private var topPaddingView: some View {
|
||||||
HStack {}
|
HStack {}
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import SwiftUI
|
||||||
@Observable class NotificationsViewModel {
|
@Observable class NotificationsViewModel {
|
||||||
public enum State {
|
public enum State {
|
||||||
public enum PagingState {
|
public enum PagingState {
|
||||||
case none, hasNextPage, loadingNextPage
|
case none, hasNextPage
|
||||||
}
|
}
|
||||||
|
|
||||||
case loading
|
case loading
|
||||||
|
@ -143,11 +143,9 @@ import SwiftUI
|
||||||
return allNotifications
|
return allNotifications
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNextPage() async {
|
func fetchNextPage() async throws {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
|
||||||
guard let lastId = consolidatedNotifications.last?.notificationIds.last else { return }
|
guard let lastId = consolidatedNotifications.last?.notificationIds.last else { return }
|
||||||
state = .display(notifications: consolidatedNotifications, nextPageState: .loadingNextPage)
|
|
||||||
let newNotifications: [Models.Notification] =
|
let newNotifications: [Models.Notification] =
|
||||||
try await client.get(endpoint: Notifications.notifications(minId: nil,
|
try await client.get(endpoint: Notifications.notifications(minId: nil,
|
||||||
maxId: lastId,
|
maxId: lastId,
|
||||||
|
@ -159,9 +157,6 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
state = .display(notifications: consolidatedNotifications,
|
state = .display(notifications: consolidatedNotifications,
|
||||||
nextPageState: newNotifications.count < Constants.notificationLimit ? .none : .hasNextPage)
|
nextPageState: newNotifications.count < Constants.notificationLimit ? .none : .hasNextPage)
|
||||||
} catch {
|
|
||||||
state = .error(error: error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func markAsRead() {
|
func markAsRead() {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import SwiftUI
|
||||||
|
|
||||||
public enum StatusesState {
|
public enum StatusesState {
|
||||||
public enum PagingState {
|
public enum PagingState {
|
||||||
case hasNextPage, loadingNextPage, none
|
case hasNextPage, none
|
||||||
}
|
}
|
||||||
|
|
||||||
case loading
|
case loading
|
||||||
|
@ -17,7 +17,7 @@ public enum StatusesState {
|
||||||
public protocol StatusesFetcher {
|
public protocol StatusesFetcher {
|
||||||
var statusesState: StatusesState { get }
|
var statusesState: StatusesState { get }
|
||||||
func fetchNewestStatuses(pullToRefresh: Bool) async
|
func fetchNewestStatuses(pullToRefresh: Bool) async
|
||||||
func fetchNextPage() async
|
func fetchNextPage() async throws
|
||||||
func statusDidAppear(status: Status)
|
func statusDidAppear(status: Status)
|
||||||
func statusDidDisappear(status: Status)
|
func statusDidDisappear(status: Status)
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,31 +60,17 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
}
|
}
|
||||||
switch nextPageState {
|
switch nextPageState {
|
||||||
case .hasNextPage:
|
case .hasNextPage:
|
||||||
loadingRow
|
NextPageView {
|
||||||
.id(UUID().uuidString)
|
try await fetcher.fetchNextPage()
|
||||||
.onAppear {
|
|
||||||
Task {
|
|
||||||
await fetcher.fetchNextPage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .loadingNextPage:
|
|
||||||
loadingRow
|
|
||||||
.id(UUID().uuidString)
|
|
||||||
case .none:
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var loadingRow: some View {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
ProgressView()
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, .layoutPadding)
|
.padding(.horizontal, .layoutPadding)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
case .none:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -417,12 +417,13 @@ extension TimelineViewModel: StatusesFetcher {
|
||||||
return allStatuses
|
return allStatuses
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNextPage() async {
|
enum NextPageError: Error {
|
||||||
guard let client else { return }
|
case internalError
|
||||||
do {
|
}
|
||||||
|
|
||||||
|
func fetchNextPage() async throws {
|
||||||
let statuses = await datasource.get()
|
let statuses = await datasource.get()
|
||||||
guard let lastId = statuses.last?.id else { return }
|
guard let client, let lastId = statuses.last?.id else { throw NextPageError.internalError }
|
||||||
statusesState = await .display(statuses: datasource.getFiltered(), nextPageState: .loadingNextPage)
|
|
||||||
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||||
maxId: lastId,
|
maxId: lastId,
|
||||||
minId: nil,
|
minId: nil,
|
||||||
|
@ -434,9 +435,6 @@ extension TimelineViewModel: StatusesFetcher {
|
||||||
|
|
||||||
statusesState = await .display(statuses: datasource.getFiltered(),
|
statusesState = await .display(statuses: datasource.getFiltered(),
|
||||||
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
|
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
|
||||||
} catch {
|
|
||||||
statusesState = .error(error: error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusDidAppear(status: Status) {
|
func statusDidAppear(status: Status) {
|
||||||
|
|
Loading…
Reference in a new issue