Generic Statuses list

This commit is contained in:
Thomas Ricouard 2022-12-19 07:17:01 +01:00
parent 4c3809a95b
commit e2455a472e
7 changed files with 173 additions and 109 deletions

View file

@ -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 {

View file

@ -1,9 +1,10 @@
import SwiftUI
import Network
import Models
import Status
@MainActor
class AccountDetailViewModel: ObservableObject {
class AccountDetailViewModel: ObservableObject, StatusesFetcher {
let accountId: String
var client: Client = .init(server: "")
@ -11,14 +12,6 @@ class AccountDetailViewModel: ObservableObject {
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)

View file

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Status"
BuildableName = "Status"
BlueprintName = "Status"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "StatusTests"
BuildableName = "StatusTests"
BlueprintName = "StatusTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Status"
BuildableName = "Status"
BlueprintName = "Status"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -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")
]),
]
)

View file

@ -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<Fetcher>: 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()
}
}
}

View file

@ -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 {

View file

@ -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)
}
}
}