Files
RSSuper/iOS/RSSuper/UI/FeedListView.swift
Michael Freno ba1e2e96e7 feat: implement iOS UI integration with ViewModels
- Add SwiftUI views for feed list, detail, add feed, settings, and bookmarks
- Connect all views to ViewModels using @StateObject
- Implement pull-to-refresh for feed list
- Add error handling and loading states to all views
- Create FeedItemRow view for consistent feed item display
- Add toFeedItem() extension to Bookmark for UI integration
- Update FeedDetailView to use sync methods
- Update BookmarkView to use FeedService for unstar operations

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-31 06:50:11 -04:00

128 lines
4.0 KiB
Swift

import SwiftUI
struct FeedListView: View {
@StateObject private var viewModel: FeedViewModel
@State private var selectedFeedItem: FeedItem?
@State private var showError: Bool = false
@State private var errorMessage: String = ""
private let feedService: FeedServiceProtocol
init(feedService: FeedServiceProtocol = FeedService()) {
self.feedService = feedService
_viewModel = StateObject(wrappedValue: FeedViewModel(feedService: feedService))
}
var body: some View {
NavigationSplitView {
List {
switch viewModel.feedState {
case .idle:
ContentUnavailableView("No Feed", systemImage: "rss")
.padding()
case .loading:
ProgressView("Loading...")
.padding()
case .success(let items):
ForEach(items) { item in
FeedItemRow(feedItem: item)
.onTapGesture {
selectedFeedItem = item
}
}
.onDelete(perform: deleteItems)
case .error(let error):
VStack {
Text("Error loading feed")
.foregroundColor(.red)
Text(error)
.font(.caption)
Button("Retry") {
refresh()
}
}
.padding()
}
}
.navigationTitle("Feeds")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: refresh) {
Image(systemName: "arrow.clockwise")
}
}
}
.refreshable {
refresh()
}
.sheet(item: $selectedFeedItem) { item in
FeedDetailView(feedItem: item, feedService: feedService)
}
.alert("Error", isPresented: $showError) {
Button("OK", role: .cancel) { errorMessage = "" }
} message: {
Text(errorMessage)
}
}
}
private func refresh() {
if let subscriptionId = viewModel.currentSubscriptionId {
viewModel.refresh(subscriptionId: subscriptionId)
}
}
private func deleteItems(offsets: IndexSet) {
guard let subscriptionId = viewModel.currentSubscriptionId else { return }
Task {
let items = viewModel.feedItems
for index in offsets {
let item = items[index]
_ = feedService.markItemAsRead(itemId: item.id)
}
viewModel.refresh(subscriptionId: subscriptionId)
}
}
}
struct FeedItemRow: View {
let feedItem: FeedItem
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(feedItem.title)
.font(.headline)
.lineLimit(2)
if let author = feedItem.author {
Text(author)
.font(.subheadline)
.foregroundColor(.secondary)
}
if let published = feedItem.published {
Text(published, format: Date.FormatStyle(date: .abbreviated, time: .shortened))
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
if !feedItem.read {
Circle()
.fill(Color.blue)
.frame(width: 8, height: 8)
}
}
.padding(.vertical, 4)
}
}
#Preview {
FeedListView()
}