diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index 10c5abe6..812797b5 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -21,8 +21,7 @@ public struct AccountDetailView: View { ScrollView { LazyVStack { headerView - statusesView - .padding(.horizontal, 16) + StatusesListView(fetcher: viewModel) } } .edgesIgnoringSafeArea(.top) @@ -46,47 +45,6 @@ public struct AccountDetailView: View { } } - - @ViewBuilder - private var statusesView: some View { - switch viewModel.statusesState { - case .loading: - ForEach(Status.placeholders()) { status in - StatusRowView(status: status) - .redacted(reason: .placeholder) - .shimmering() - Divider() - } - case let .error(error): - Text(error.localizedDescription) - case let .display(statuses, nextPageState): - ForEach(statuses) { status in - StatusRowView(status: status) - Divider() - .padding(.bottom, DS.Constants.layoutPadding) - } - - switch nextPageState { - case .hasNextPage: - loadingRow - .onAppear { - Task { - await viewModel.loadNextPage() - } - } - case .loadingNextPage: - loadingRow - } - } - } - - private var loadingRow: some View { - HStack { - Spacer() - ProgressView() - Spacer() - } - } } struct AccountDetailView_Previews: PreviewProvider { diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index 9ae11e2a..03d9c008 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -1,24 +1,17 @@ import SwiftUI import Network import Models +import Status @MainActor -class AccountDetailViewModel: ObservableObject { +class AccountDetailViewModel: ObservableObject, StatusesFetcher { let accountId: String var client: Client = .init(server: "") enum State { case loading, data(account: Account), error(error: Error) } - - enum StatusesState { - enum PagingState { - case hasNextPage, loadingNextPage - } - case loading - case display(statuses: [Status], nextPageState: StatusesState.PagingState) - case error(error: Error) - } + @Published var state: State = .loading @Published var statusesState: StatusesState = .loading @@ -52,7 +45,7 @@ class AccountDetailViewModel: ObservableObject { } } - func loadNextPage() async { + func fetchNextPage() async { do { guard let lastId = statuses.last?.id else { return } statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage) diff --git a/Packages/Status/.swiftpm/xcode/xcshareddata/xcschemes/Status.xcscheme b/Packages/Status/.swiftpm/xcode/xcshareddata/xcschemes/Status.xcscheme new file mode 100644 index 00000000..d245aea2 --- /dev/null +++ b/Packages/Status/.swiftpm/xcode/xcshareddata/xcschemes/Status.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Packages/Status/Package.swift b/Packages/Status/Package.swift index 0f1d4830..b1a8930f 100644 --- a/Packages/Status/Package.swift +++ b/Packages/Status/Package.swift @@ -17,6 +17,7 @@ let package = Package( .package(name: "Models", path: "../Models"), .package(name: "Routeur", path: "../Routeur"), .package(name: "DesignSystem", path: "../DesignSystem"), + .package(url: "https://github.com/markiv/SwiftUI-Shimmer", exact: "1.1.0") ], targets: [ .target( @@ -25,6 +26,7 @@ let package = Package( .product(name: "Models", package: "Models"), .product(name: "Routeur", package: "Routeur"), .product(name: "DesignSystem", package: "DesignSystem"), + .product(name: "Shimmer", package: "SwiftUI-Shimmer") ]), ] ) diff --git a/Packages/Status/Sources/Status/StatusesListView.swift b/Packages/Status/Sources/Status/StatusesListView.swift new file mode 100644 index 00000000..7da6299e --- /dev/null +++ b/Packages/Status/Sources/Status/StatusesListView.swift @@ -0,0 +1,72 @@ +import SwiftUI +import Models +import Shimmer +import DesignSystem + +public enum StatusesState { + public enum PagingState { + case hasNextPage, loadingNextPage + } + case loading + case display(statuses: [Status], nextPageState: StatusesState.PagingState) + case error(error: Error) +} + +@MainActor +public protocol StatusesFetcher: ObservableObject { + var statusesState: StatusesState { get } + func fetchStatuses() async + func fetchNextPage() async +} + +public struct StatusesListView: View where Fetcher: StatusesFetcher { + @ObservedObject private var fetcher: Fetcher + + public init(fetcher: Fetcher) { + self.fetcher = fetcher + } + + public var body: some View { + Group { + switch fetcher.statusesState { + case .loading: + ForEach(Status.placeholders()) { status in + StatusRowView(status: status) + .redacted(reason: .placeholder) + .shimmering() + Divider() + .padding(.bottom, DS.Constants.layoutPadding) + } + case let .error(error): + Text(error.localizedDescription) + case let .display(statuses, nextPageState): + ForEach(statuses) { status in + StatusRowView(status: status) + Divider() + .padding(.bottom, DS.Constants.layoutPadding) + } + + switch nextPageState { + case .hasNextPage: + loadingRow + .onAppear { + Task { + await fetcher.fetchNextPage() + } + } + case .loadingNextPage: + loadingRow + } + } + } + .padding(.horizontal, 16) + } + + private var loadingRow: some View { + HStack { + Spacer() + ProgressView() + Spacer() + } + } +} diff --git a/Packages/Timeline/Sources/Timeline/TimelineView.swift b/Packages/Timeline/Sources/Timeline/TimelineView.swift index bc8fe37d..1ea0086f 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -12,34 +12,11 @@ public struct TimelineView: View { public init() {} public var body: some View { - List { - switch viewModel.state { - case .loading: - ForEach(Status.placeholders()) { placeholder in - StatusRowView(status: placeholder) - .redacted(reason: .placeholder) - .shimmering() - } - case let .error(error): - Text(error.localizedDescription) - case let .display(statuses, nextPageState): - ForEach(statuses) { status in - StatusRowView(status: status) - } - switch nextPageState { - case .hasNextPage: - loadingRow - .onAppear { - Task { - await viewModel.loadNextPage() - } - } - case .loadingNextPage: - loadingRow - } + ScrollView { + LazyVStack { + StatusesListView(fetcher: viewModel) } } - .listStyle(.plain) .navigationTitle(viewModel.timeline.rawValue) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -50,22 +27,15 @@ public struct TimelineView: View { .task { viewModel.client = client if !didAppear { - await viewModel.refreshTimeline() + await viewModel.fetchStatuses() didAppear = true } } .refreshable { - await viewModel.refreshTimeline() + await viewModel.fetchStatuses() } } - private var loadingRow: some View { - HStack { - Spacer() - ProgressView() - Spacer() - } - } private var timelineFilterButton: some View { Menu { diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index a02103e1..85439022 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -1,18 +1,10 @@ import SwiftUI import Network import Models +import Status @MainActor -class TimelineViewModel: ObservableObject { - enum State { - enum PagingState { - case hasNextPage, loadingNextPage - } - case loading - case display(statuses: [Status], nextPageState: State.PagingState) - case error(error: Error) - } - +class TimelineViewModel: ObservableObject, StatusesFetcher { enum TimelineFilter: String, CaseIterable { case pub = "Public" case home = "Home" @@ -33,12 +25,12 @@ class TimelineViewModel: ObservableObject { private var statuses: [Status] = [] - @Published var state: State = .loading + @Published var statusesState: StatusesState = .loading @Published var timeline: TimelineFilter = .pub { didSet { if oldValue != timeline { Task { - await refreshTimeline() + await fetchStatuses() } } } @@ -48,25 +40,25 @@ class TimelineViewModel: ObservableObject { client.server } - func refreshTimeline() async { + func fetchStatuses() async { do { - state = .loading + statusesState = .loading statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil)) - state = .display(statuses: statuses, nextPageState: .hasNextPage) + statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) } catch { - state = .error(error: error) + statusesState = .error(error: error) } } - func loadNextPage() async { + func fetchNextPage() async { do { guard let lastId = statuses.last?.id else { return } - state = .display(statuses: statuses, nextPageState: .loadingNextPage) + statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage) let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: lastId)) statuses.append(contentsOf: newStatuses) - state = .display(statuses: statuses, nextPageState: .hasNextPage) + statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) } catch { - state = .error(error: error) + statusesState = .error(error: error) } } }