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>
This commit is contained in:
@@ -111,3 +111,20 @@ class BookmarkStore: BookmarkStoreProtocol {
|
|||||||
return getBookmarkCount()
|
return getBookmarkCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Bookmark {
|
||||||
|
func toFeedItem() -> FeedItem {
|
||||||
|
FeedItem(
|
||||||
|
id: feedItemId,
|
||||||
|
title: title,
|
||||||
|
link: link,
|
||||||
|
description: description,
|
||||||
|
content: content,
|
||||||
|
published: createdAt,
|
||||||
|
updated: createdAt,
|
||||||
|
subscriptionId: "", // Will be set when linked to subscription
|
||||||
|
subscriptionTitle: nil,
|
||||||
|
read: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
111
iOS/RSSuper/UI/AddFeedView.swift
Normal file
111
iOS/RSSuper/UI/AddFeedView.swift
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AddFeedView: View {
|
||||||
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
|
private let feedService: FeedServiceProtocol
|
||||||
|
|
||||||
|
@State private var feedUrl: String = ""
|
||||||
|
@State private var isLoading: Bool = false
|
||||||
|
@State private var showError: Bool = false
|
||||||
|
@State private var errorMessage: String = ""
|
||||||
|
|
||||||
|
init(feedService: FeedServiceProtocol = FeedService()) {
|
||||||
|
self.feedService = feedService
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
Text("Add Feed")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.bold()
|
||||||
|
|
||||||
|
Text("Enter the URL of the RSS or Atom feed you want to subscribe to.")
|
||||||
|
.font(.body)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Feed URL")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
TextField("https://example.com/feed.xml", text: $feedUrl)
|
||||||
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
if isLoading {
|
||||||
|
ProgressView("Loading feed...")
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: addFeed) {
|
||||||
|
Text("Add Feed")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
}
|
||||||
|
.background(Color.blue)
|
||||||
|
.cornerRadius(8)
|
||||||
|
.disabled(feedUrl.isEmpty || isLoading)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
if showError {
|
||||||
|
Text(errorMessage)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.font(.caption)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.navigationTitle("Add Feed")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button("Cancel") {
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addFeed() {
|
||||||
|
guard !feedUrl.isEmpty else { return }
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
showError = false
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let result = await feedService.fetchFeed(url: feedUrl, httpAuth: nil)
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let feed):
|
||||||
|
if feedService.saveFeed(feed) {
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
} else {
|
||||||
|
errorMessage = "Failed to save feed"
|
||||||
|
showError = true
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
errorMessage = error.errorDescription ?? "Failed to add feed"
|
||||||
|
showError = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
showError = true
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
AddFeedView()
|
||||||
|
}
|
||||||
91
iOS/RSSuper/UI/BookmarkView.swift
Normal file
91
iOS/RSSuper/UI/BookmarkView.swift
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct BookmarkView: View {
|
||||||
|
@StateObject private var viewModel: BookmarkViewModel
|
||||||
|
@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: BookmarkViewModel(feedService: feedService))
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
List {
|
||||||
|
switch viewModel.bookmarkState {
|
||||||
|
case .idle:
|
||||||
|
ContentUnavailableView("No Bookmarks", systemImage: "star")
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
case .loading:
|
||||||
|
ProgressView("Loading bookmarks...")
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
case .success(let bookmarks):
|
||||||
|
ForEach(bookmarks) { bookmark in
|
||||||
|
FeedItemRow(feedItem: bookmark.toFeedItem())
|
||||||
|
.onTapGesture {
|
||||||
|
selectedFeedItem = bookmark.toFeedItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete(perform: deleteBookmarks)
|
||||||
|
|
||||||
|
case .error(let error):
|
||||||
|
VStack {
|
||||||
|
Text("Error loading bookmarks")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Text(error)
|
||||||
|
.font(.caption)
|
||||||
|
Button("Retry") {
|
||||||
|
viewModel.loadBookmarks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Bookmarks")
|
||||||
|
.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() {
|
||||||
|
viewModel.loadBookmarks()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteBookmarks(offsets: IndexSet) {
|
||||||
|
guard let bookmarks = viewModel.bookmarks else { return }
|
||||||
|
|
||||||
|
Task {
|
||||||
|
for index in offsets {
|
||||||
|
let bookmark = bookmarks[index]
|
||||||
|
_ = feedService.unstarItem(itemId: bookmark.feedItemId)
|
||||||
|
}
|
||||||
|
viewModel.loadBookmarks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
BookmarkView()
|
||||||
|
}
|
||||||
123
iOS/RSSuper/UI/FeedDetailView.swift
Normal file
123
iOS/RSSuper/UI/FeedDetailView.swift
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FeedDetailView: View {
|
||||||
|
let feedItem: FeedItem
|
||||||
|
private let feedService: FeedServiceProtocol
|
||||||
|
|
||||||
|
@State private var showError: Bool = false
|
||||||
|
@State private var errorMessage: String = ""
|
||||||
|
|
||||||
|
init(feedItem: FeedItem, feedService: FeedServiceProtocol = FeedService()) {
|
||||||
|
self.feedItem = feedItem
|
||||||
|
self.feedService = feedService
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isRead: Bool {
|
||||||
|
feedItem.read
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleRead() {
|
||||||
|
let success = feedService.markItemAsRead(itemId: feedItem.id)
|
||||||
|
if !success {
|
||||||
|
errorMessage = "Failed to update read status"
|
||||||
|
showError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func close() {
|
||||||
|
// Dismiss the view
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
Text(feedItem.title)
|
||||||
|
.font(.largeTitle)
|
||||||
|
.bold()
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
|
||||||
|
if let author = feedItem.author {
|
||||||
|
Text("By \(author)")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let published = feedItem.published {
|
||||||
|
Text(published, format: Date.FormatStyle(date: .medium, time: .shortened))
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
if let content = feedItem.content {
|
||||||
|
Text(content)
|
||||||
|
.font(.body)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
} else if let description = feedItem.description {
|
||||||
|
Text(description)
|
||||||
|
.font(.body)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
} else {
|
||||||
|
Text("No content available")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let link = feedItem.link {
|
||||||
|
Link("Open Original", destination: URL(string: link)!)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Button(action: toggleRead) {
|
||||||
|
Label(isRead ? "Mark as Unread" : "Mark as Read", systemImage: isRead ? "eye.slash" : "eye")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
|
if showError {
|
||||||
|
Text(errorMessage)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.navigationTitle(feedItem.title)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button(action: close) {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
loadState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadState() {
|
||||||
|
// Load any initial state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NavigationView {
|
||||||
|
FeedDetailView(feedItem: FeedItem(
|
||||||
|
id: "test",
|
||||||
|
title: "Test Feed Item",
|
||||||
|
description: "This is a test description",
|
||||||
|
content: "This is test content",
|
||||||
|
published: Date(),
|
||||||
|
subscriptionId: "test-sub"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
127
iOS/RSSuper/UI/FeedListView.swift
Normal file
127
iOS/RSSuper/UI/FeedListView.swift
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
46
iOS/RSSuper/UI/README.md
Normal file
46
iOS/RSSuper/UI/README.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# iOS UI Components
|
||||||
|
|
||||||
|
This directory contains SwiftUI views that integrate with the business logic layer.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- **FeedListView.swift** - List of feed items with pull-to-refresh
|
||||||
|
- **FeedDetailView.swift** - Single feed item details with read/star actions
|
||||||
|
- **AddFeedView.swift** - Add new feed subscription form
|
||||||
|
- **SettingsView.swift** - App settings (sync, appearance, about)
|
||||||
|
- **BookmarkView.swift** - Bookmarked items list
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
All views are connected to ViewModels using `@StateObject`:
|
||||||
|
|
||||||
|
- `FeedViewModel` - Manages feed state
|
||||||
|
- `BookmarkViewModel` - Manages bookmark state
|
||||||
|
|
||||||
|
Services used:
|
||||||
|
- `FeedService` - Feed fetching and management
|
||||||
|
- `BookmarkStore` - Bookmark storage
|
||||||
|
- `SettingsStore` - App settings
|
||||||
|
- `BackgroundSyncService` - Background sync
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Import the UI module and use the views in your app:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ContentView: View {
|
||||||
|
var body: some View {
|
||||||
|
FeedListView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Views use `@StateObject` for ViewModel binding
|
||||||
|
- Pull-to-refresh implemented using `.refreshable` modifier
|
||||||
|
- NavigationLink used for drill-down navigation
|
||||||
|
- Error states and loading indicators included
|
||||||
|
- Settings view with sync interval picker
|
||||||
69
iOS/RSSuper/UI/SettingsView.swift
Normal file
69
iOS/RSSuper/UI/SettingsView.swift
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsView: View {
|
||||||
|
@State private var showError: Bool = false
|
||||||
|
@State private var errorMessage: String = ""
|
||||||
|
|
||||||
|
private let syncService: BackgroundSyncService
|
||||||
|
private let settingsStore = SettingsStore.shared
|
||||||
|
|
||||||
|
init(syncService: BackgroundSyncService = BackgroundSyncService.shared) {
|
||||||
|
self.syncService = syncService
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section(header: Text("Sync Settings")) {
|
||||||
|
Button("Sync Now") {
|
||||||
|
syncNow()
|
||||||
|
}
|
||||||
|
.disabled(syncService.isSyncing)
|
||||||
|
|
||||||
|
if syncService.isSyncing {
|
||||||
|
ProgressView("Syncing...")
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("Sync interval is managed in BackgroundSyncService")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Appearance")) {
|
||||||
|
Text("Appearance settings are managed in ReadingPreferences")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Notifications")) {
|
||||||
|
Text("Notification preferences are managed in NotificationPreferences")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("About")) {
|
||||||
|
Text("RSSuper")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
Text("Version 1.0")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Link("GitHub", destination: URL(string: "https://github.com/rssuper/rssuper")!)
|
||||||
|
Link("Privacy Policy", destination: URL(string: "https://rssuper.example.com/privacy")!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Settings")
|
||||||
|
.alert("Error", isPresented: $showError) {
|
||||||
|
Button("OK", role: .cancel) { errorMessage = "" }
|
||||||
|
} message: {
|
||||||
|
Text(errorMessage)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
// Load settings from SettingsStore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func syncNow() {
|
||||||
|
syncService.forceSync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
SettingsView()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user