mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-25 17:51:01 +00:00
Generic Statuses list
This commit is contained in:
parent
4c3809a95b
commit
e2455a472e
7 changed files with 173 additions and 109 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
|
@ -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")
|
||||
]),
|
||||
]
|
||||
)
|
||||
|
|
72
Packages/Status/Sources/Status/StatusesListView.swift
Normal file
72
Packages/Status/Sources/Status/StatusesListView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
StatusesListView(fetcher: viewModel)
|
||||
}
|
||||
}
|
||||
case .loadingNextPage:
|
||||
loadingRow
|
||||
}
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue