From 28cecf585c8a80e9457a857a0a58fb21a27a586f Mon Sep 17 00:00:00 2001 From: Ilyas Hallak Date: Sat, 14 Jun 2025 22:25:19 +0200 Subject: [PATCH] feat: Implement native List view with swipe actions and Safari integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace ScrollView + LazyVStack with native List for better performance - Add swipe actions for bookmark management (archive left, favorite/delete right) - Implement programmatic navigation with NavigationStack and navigationDestination - Create reusable SafariUtil for opening URLs in SFSafariViewController - Add clipboard URL detection and paste functionality in AddBookmarkView - Improve BookmarkCardView layout with better image handling and meta info - Add comprehensive date formatting with relative time display - Implement proper progress bar visualization for reading progress Navigation improvements: - Use Button + navigationDestination instead of NavigationLink - Add selectedBookmarkId state management for programmatic navigation - Support for share extension URL handling with notification system UI/UX enhancements: - Native iOS swipe gestures with haptic feedback - Consistent Safari integration across all views - Better accessibility with proper button targets - Improved visual hierarchy with refined spacing and typography - Added image fallback chain (image → thumbnail → icon → placeholder) Technical changes: - Remove ScrollView scroll tracking complexity - Simplify FAB button logic (always visible on unread tab) - Add String+Identifiable extension for navigation - Refactor duplicate Safari opening code into utility class --- .../Data/Repository/BookmarksRepository.swift | 2 +- readeck/UI/AddBookmark/AddBookmarkView.swift | 295 ++++++++++++------ .../UI/AddBookmark/AddBookmarkViewModel.swift | 6 +- .../BookmarkDetail/BookmarkDetailView.swift | 16 +- readeck/UI/Bookmarks/BookmarkCardView.swift | 130 +++----- readeck/UI/Bookmarks/BookmarksView.swift | 129 ++++---- readeck/Utils/SafariUtil.swift | 25 ++ 7 files changed, 357 insertions(+), 246 deletions(-) create mode 100644 readeck/Utils/SafariUtil.swift diff --git a/readeck/Data/Repository/BookmarksRepository.swift b/readeck/Data/Repository/BookmarksRepository.swift index cef39c7..421f76d 100644 --- a/readeck/Data/Repository/BookmarksRepository.swift +++ b/readeck/Data/Repository/BookmarksRepository.swift @@ -56,7 +56,7 @@ class BookmarksRepository: PBookmarksRepository { let response = try await api.createBookmark(createRequest: dto) // Prüfe ob die Erstellung erfolgreich war - guard response.status == 0 else { + guard response.status == 0 || response.status == 202 else { throw CreateBookmarkError.serverError(response.message) } diff --git a/readeck/UI/AddBookmark/AddBookmarkView.swift b/readeck/UI/AddBookmark/AddBookmarkView.swift index cdff338..2ba5b15 100644 --- a/readeck/UI/AddBookmark/AddBookmarkView.swift +++ b/readeck/UI/AddBookmark/AddBookmarkView.swift @@ -5,124 +5,193 @@ struct AddBookmarkView: View { @Environment(\.dismiss) private var dismiss init(prefilledURL: String? = nil, prefilledTitle: String? = nil) { - viewModel.title = prefilledTitle ?? "" - viewModel.url = prefilledURL ?? "" + _viewModel = State(initialValue: AddBookmarkViewModel()) + if let url = prefilledURL { + viewModel.url = url + } + if let title = prefilledTitle { + viewModel.title = title + } } var body: some View { NavigationView { - Form { - Section(header: Text("Bookmark Details")) { - VStack(alignment: .leading, spacing: 8) { - Text("URL *") - .font(.caption) - .foregroundColor(.secondary) + VStack(spacing: 0) { + // Scrollable Form Content + ScrollView { + VStack(spacing: 24) { + // Header + VStack(spacing: 8) { + Image(systemName: "bookmark.circle.fill") + .font(.system(size: 48)) + .foregroundColor(.accentColor) + + Text("Neues Bookmark") + .font(.title2) + .fontWeight(.semibold) + + Text("Füge einen neuen Link zu deiner Sammlung hinzu") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .padding(.top, 20) - TextField("https://example.com", text: $viewModel.url) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .keyboardType(.URL) - .autocapitalization(.none) - .autocorrectionDisabled() - } - - VStack(alignment: .leading, spacing: 8) { - Text("Titel (optional)") - .font(.caption) - .foregroundColor(.secondary) - - TextField("Bookmark Titel", text: $viewModel.title) - .textFieldStyle(RoundedBorderTextFieldStyle()) - } - } - - Section(header: Text("Labels")) { - VStack(alignment: .leading, spacing: 8) { - Text("Labels (durch Komma getrennt)") - .font(.caption) - .foregroundColor(.secondary) - - TextField("work, important, later", text: $viewModel.labelsText) - .textFieldStyle(RoundedBorderTextFieldStyle()) - } - - if !viewModel.parsedLabels.isEmpty { - LazyVGrid(columns: [ - GridItem(.adaptive(minimum: 80)) - ], spacing: 8) { - ForEach(viewModel.parsedLabels, id: \.self) { label in - Text(label) - .font(.caption) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.blue.opacity(0.1)) - .foregroundColor(.blue) - .clipShape(Capsule()) + // Form Fields + VStack(spacing: 20) { + // URL Field + VStack(alignment: .leading, spacing: 8) { + HStack { + Label("URL", systemImage: "link") + .font(.headline) + .foregroundColor(.primary) + + Spacer() + + Text("Erforderlich") + .font(.caption) + .foregroundColor(.red) + } + + TextField("https://example.com", text: $viewModel.url) + .textFieldStyle(CustomTextFieldStyle()) + .keyboardType(.URL) + .autocapitalization(.none) + .autocorrectionDisabled() + } + + // Title Field + VStack(alignment: .leading, spacing: 8) { + Label("Titel", systemImage: "note.text") + .font(.headline) + .foregroundColor(.primary) + + TextField("Optional: Eigener Titel", text: $viewModel.title) + .textFieldStyle(CustomTextFieldStyle()) + } + + // Labels Field + VStack(alignment: .leading, spacing: 8) { + Label("Labels", systemImage: "tag") + .font(.headline) + .foregroundColor(.primary) + + TextField("z.B. arbeit, wichtig, später", text: $viewModel.labelsText) + .textFieldStyle(CustomTextFieldStyle()) + + // Labels Preview + if !viewModel.parsedLabels.isEmpty { + LazyVGrid(columns: [ + GridItem(.adaptive(minimum: 80)) + ], spacing: 8) { + ForEach(viewModel.parsedLabels, id: \.self) { label in + Text(label) + .font(.caption) + .fontWeight(.medium) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.accentColor.opacity(0.1)) + .foregroundColor(.accentColor) + .clipShape(Capsule()) + } + } + .padding(.top, 8) + } + } + + // Clipboard Section + if viewModel.clipboardURL != nil { + VStack(alignment: .leading, spacing: 12) { + Label("Zwischenablage", systemImage: "doc.on.clipboard") + .font(.headline) + .foregroundColor(.primary) + + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("URL gefunden:") + .font(.caption) + .foregroundColor(.secondary) + + Text(viewModel.clipboardURL ?? "") + .font(.subheadline) + .lineLimit(2) + .truncationMode(.middle) + } + + Spacer() + + Button("Einfügen") { + viewModel.pasteFromClipboard() + } + .buttonStyle(SecondaryButtonStyle()) + } + .padding() + .background(Color(.systemGray6)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } } } + .padding(.horizontal, 20) + + Spacer(minLength: 100) // Platz für Button } } - Section { - Button("Aus Zwischenablage einfügen") { - viewModel.pasteFromClipboard() - } - .disabled(viewModel.clipboardURL == nil) + // Bottom Action Area + VStack(spacing: 16) { + Divider() - if let clipboardURL = viewModel.clipboardURL { - Text("Zwischenablage: \(clipboardURL)") - .font(.caption) - .foregroundColor(.secondary) - .lineLimit(1) - .truncationMode(.middle) + VStack(spacing: 12) { + // Save Button + Button(action: { + Task { + await viewModel.createBookmark() + if viewModel.hasCreated { + dismiss() + } + } + }) { + HStack { + if viewModel.isLoading { + ProgressView() + .scaleEffect(0.8) + .foregroundColor(.white) + } else { + Image(systemName: "bookmark.fill") + } + + Text(viewModel.isLoading ? "Wird gespeichert..." : "Bookmark speichern") + .fontWeight(.semibold) + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(viewModel.isValid && !viewModel.isLoading ? Color.accentColor : Color.gray) + .foregroundColor(.white) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } + .disabled(!viewModel.isValid || viewModel.isLoading) + + // Cancel Button + Button("Abbrechen") { + dismiss() + viewModel.clearForm() + } + .foregroundColor(.secondary) } + .padding(.horizontal, 20) + .padding(.bottom, 20) } + .background(Color(.systemBackground)) } - .navigationTitle("Bookmark hinzufügen") .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Abbrechen") { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Schließen") { dismiss() viewModel.clearForm() } + .foregroundColor(.secondary) } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("Speichern") { - Task { - await viewModel.createBookmark() - dismiss() - } - } - .disabled(!viewModel.isValid || viewModel.isLoading) - } - } - .overlay { - if viewModel.isLoading { - ZStack { - Color.black.opacity(0.3) - - VStack(spacing: 16) { - ProgressView() - .scaleEffect(1.2) - - Text("Bookmark wird erstellt...") - .font(.subheadline) - } - .padding(24) - .background(Color(.systemBackground)) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .shadow(radius: 10) - } - .ignoresSafeArea() - } - } - .alert("Erfolgreich", isPresented: $viewModel.showSuccessAlert) { - Button("OK") { - dismiss() - } - } message: { - Text("Bookmark wurde erfolgreich hinzugefügt!") } .alert("Fehler", isPresented: $viewModel.showErrorAlert) { Button("OK", role: .cancel) { } @@ -139,6 +208,36 @@ struct AddBookmarkView: View { } } +// MARK: - Custom Styles + +struct CustomTextFieldStyle: TextFieldStyle { + func _body(configuration: TextField) -> some View { + configuration + .padding() + .background(Color(.systemGray6)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color(.systemGray4), lineWidth: 1) + ) + } +} + +struct SecondaryButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(Color.accentColor.opacity(0.1)) + .foregroundColor(.accentColor) + .clipShape(Capsule()) + .scaleEffect(configuration.isPressed ? 0.95 : 1.0) + .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) + } +} + #Preview { AddBookmarkView() } diff --git a/readeck/UI/AddBookmark/AddBookmarkViewModel.swift b/readeck/UI/AddBookmark/AddBookmarkViewModel.swift index be8d0fa..98d9df8 100644 --- a/readeck/UI/AddBookmark/AddBookmarkViewModel.swift +++ b/readeck/UI/AddBookmark/AddBookmarkViewModel.swift @@ -12,7 +12,7 @@ class AddBookmarkViewModel { var isLoading: Bool = false var errorMessage: String? var showErrorAlert: Bool = false - var showSuccessAlert: Bool = false + var hasCreated: Bool = false var clipboardURL: String? var isValid: Bool { @@ -33,6 +33,7 @@ class AddBookmarkViewModel { isLoading = true errorMessage = nil + hasCreated = false do { let cleanURL = url.trimmingCharacters(in: .whitespacesAndNewlines) @@ -50,7 +51,8 @@ class AddBookmarkViewModel { // Optional: Zeige die Server-Nachricht an print("Server response: \(message)") - clearForm() + clearForm() + hasCreated = true } catch let error as CreateBookmarkError { errorMessage = error.localizedDescription showErrorAlert = true diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift index 9898a73..d314a3d 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift @@ -1,4 +1,5 @@ import SwiftUI +import SafariServices struct BookmarkDetailView: View { let bookmarkId: String @@ -33,7 +34,7 @@ struct BookmarkDetailView: View { metaInfoSection Divider() - + // Artikel-Inhalt mit WebView if !viewModel.articleContent.isEmpty { WebView(htmlContent: viewModel.articleContent) { height in @@ -76,10 +77,21 @@ struct BookmarkDetailView: View { HStack { Image(systemName: "textformat") - Text("\(viewModel.bookmarkDetail.wordCount) Wörter • \(viewModel.bookmarkDetail.readingTime) min Lesezeit") + Text("\(viewModel.bookmarkDetail.wordCount ?? 0) Wörter • \(viewModel.bookmarkDetail.readingTime ?? 0) min Lesezeit") .font(.subheadline) .foregroundColor(.secondary) } + + HStack { + Image(systemName: "safari") + Button(action: { + SafariUtil.openInSafari(url: viewModel.bookmarkDetail.url) + }) { + Text("Original Seite öffnen") + .font(.subheadline) + .foregroundColor(.secondary) + } + } } } diff --git a/readeck/UI/Bookmarks/BookmarkCardView.swift b/readeck/UI/Bookmarks/BookmarkCardView.swift index 9d21dff..1340814 100644 --- a/readeck/UI/Bookmarks/BookmarkCardView.swift +++ b/readeck/UI/Bookmarks/BookmarkCardView.swift @@ -1,4 +1,5 @@ import SwiftUI +import SafariServices struct BookmarkCardView: View { let bookmark: Bookmark @@ -7,8 +8,6 @@ struct BookmarkCardView: View { let onDelete: (Bookmark) -> Void let onToggleFavorite: (Bookmark) -> Void - @State private var showingActionSheet = false - var body: some View { VStack(alignment: .leading, spacing: 8) { // Vorschaubild - verwende image oder thumbnail @@ -28,35 +27,6 @@ struct BookmarkCardView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) VStack(alignment: .leading, spacing: 4) { - // Status-Icons und Action-Button - HStack { - HStack(spacing: 6) { - if bookmark.isMarked { - IconBadge(systemName: "heart.fill", color: .red) - } - if bookmark.isArchived { - IconBadge(systemName: "archivebox.fill", color: .gray) - } - if bookmark.hasArticle { - IconBadge(systemName: "doc.text.fill", color: .green) - } - } - - Spacer() - - // Action Menu Button - Button(action: { - showingActionSheet = true - }) { - Image(systemName: "ellipsis") - .foregroundColor(.secondary) - .padding(8) - .background(Color.gray.opacity(0.1)) - .clipShape(Circle()) - } - .buttonStyle(PlainButtonStyle()) - } - // Titel Text(bookmark.title) .font(.headline) @@ -64,36 +34,37 @@ struct BookmarkCardView: View { .lineLimit(2) .multilineTextAlignment(.leading) - // Beschreibung - if !bookmark.description.isEmpty { - Text(bookmark.description) - .font(.subheadline) - .foregroundColor(.secondary) - .lineLimit(3) - .multilineTextAlignment(.leading) - } - // Meta-Info mit Datum VStack(alignment: .leading, spacing: 4) { HStack { - if !bookmark.siteName.isEmpty { - Label(bookmark.siteName, systemImage: "globe") - } - Spacer() + // Veröffentlichungsdatum + if let publishedDate = formattedPublishedDate { + HStack { + Label(publishedDate, systemImage: "calendar") + Spacer() + } + + Spacer() // show spacer only if we have the published Date + } if let readingTime = bookmark.readingTime, readingTime > 0 { Label("\(readingTime) min", systemImage: "clock") } } - // Veröffentlichungsdatum - if let publishedDate = formattedPublishedDate { - HStack { - Label(publishedDate, systemImage: "calendar") - Spacer() + HStack { + if !bookmark.siteName.isEmpty { + Label(bookmark.siteName, systemImage: "globe") } } + HStack { + + Label("Original Seite öffnen", systemImage: "safari") + .onTapGesture { + SafariUtil.openInSafari(url: bookmark.url) + } + } } .font(.caption) .foregroundColor(.secondary) @@ -111,16 +82,42 @@ struct BookmarkCardView: View { .background(Color(.systemBackground)) .clipShape(RoundedRectangle(cornerRadius: 12)) .shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1) - .confirmationDialog("Bookmark Aktionen", isPresented: $showingActionSheet) { - actionButtons + // Swipe Actions hinzufügen + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + // Löschen (ganz rechts) + Button("Löschen", role: .destructive) { + onDelete(bookmark) + } + .tint(.red) + + // Favorit (rechts) + Button { + onToggleFavorite(bookmark) + } label: { + Label(bookmark.isMarked ? "Entfernen" : "Favorit", + systemImage: bookmark.isMarked ? "heart.slash" : "heart.fill") + } + .tint(bookmark.isMarked ? .gray : .pink) + } + .swipeActions(edge: .leading, allowsFullSwipe: true) { + // Archivieren (links) + Button { + onArchive(bookmark) + } label: { + if currentState == .archived { + Label("Wiederherstellen", systemImage: "tray.and.arrow.up") + } else { + Label("Archivieren", systemImage: "archivebox") + } + } + .tint(currentState == .archived ? .blue : .orange) } } // MARK: - Computed Properties private var formattedPublishedDate: String? { - guard let published = bookmark.published, - !published.isEmpty else { + guard let published = bookmark.published, !published.isEmpty else { return nil } @@ -184,33 +181,6 @@ struct BookmarkCardView: View { return formatter.string(from: date) } - private var actionButtons: some View { - Group { - // Favorit Toggle - Button(bookmark.isMarked ? "Favorit entfernen" : "Als Favorit markieren") { - onToggleFavorite(bookmark) - } - - // Archivieren/Dearchivieren basierend auf aktuellem State - if currentState == .archived { - Button("Aus Archiv entfernen") { - onArchive(bookmark) - } - } else { - Button("Archivieren") { - onArchive(bookmark) - } - } - - // Permanent löschen (immer verfügbar) - Button("Permanent löschen", role: .destructive) { - onDelete(bookmark) - } - - Button("Abbrechen", role: .cancel) { } - } - } - private var imageURL: URL? { // Bevorzuge image, dann thumbnail, dann icon if let imageUrl = bookmark.resources.image?.src { diff --git a/readeck/UI/Bookmarks/BookmarksView.swift b/readeck/UI/Bookmarks/BookmarksView.swift index 6ac87a5..31c7438 100644 --- a/readeck/UI/Bookmarks/BookmarksView.swift +++ b/readeck/UI/Bookmarks/BookmarksView.swift @@ -3,8 +3,7 @@ import SwiftUI struct BookmarksView: View { @State private var viewModel = BookmarksViewModel() @State private var showingAddBookmark = false - @State private var scrollOffset: CGFloat = 0 - @State private var isScrolling = false + @State private var selectedBookmarkId: String? let state: BookmarkState @State private var showingAddBookmarkFromShare = false @@ -12,41 +11,43 @@ struct BookmarksView: View { @State private var shareTitle = "" var body: some View { - NavigationView { + NavigationStack { ZStack { if viewModel.isLoading && viewModel.bookmarks.isEmpty { ProgressView("Lade \(state.displayName)...") } else { - ScrollView { - LazyVStack(spacing: 12) { - ForEach(viewModel.bookmarks, id: \.id) { bookmark in - NavigationLink(destination: BookmarkDetailView(bookmarkId: bookmark.id)) { - BookmarkCardView( - bookmark: bookmark, - currentState: state, - onArchive: { bookmark in - Task { - await viewModel.toggleArchive(bookmark: bookmark) - } - }, - onDelete: { bookmark in - Task { - await viewModel.deleteBookmark(bookmark: bookmark) - } - }, - onToggleFavorite: { bookmark in - Task { - await viewModel.toggleFavorite(bookmark: bookmark) - } + List { + ForEach(viewModel.bookmarks, id: \.id) { bookmark in + Button(action: { + selectedBookmarkId = bookmark.id + }) { + BookmarkCardView( + bookmark: bookmark, + currentState: state, + onArchive: { bookmark in + Task { + await viewModel.toggleArchive(bookmark: bookmark) } - ) - .padding(.bottom, 20) - } - .buttonStyle(PlainButtonStyle()) + }, + onDelete: { bookmark in + Task { + await viewModel.deleteBookmark(bookmark: bookmark) + } + }, + onToggleFavorite: { bookmark in + Task { + await viewModel.toggleFavorite(bookmark: bookmark) + } + } + ) } + .buttonStyle(PlainButtonStyle()) + .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) } - .padding() } + .listStyle(.plain) .refreshable { await viewModel.refreshBookmarks() } @@ -60,8 +61,39 @@ struct BookmarksView: View { } } } + + // FAB Button - nur bei "Ungelesen" anzeigen + if state == .unread { + VStack { + Spacer() + HStack { + Spacer() + + Button(action: { + showingAddBookmark = true + }) { + Image(systemName: "plus") + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.white) + .frame(width: 56, height: 56) + .background(Color.accentColor) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.25), radius: 6, x: 0, y: 3) + } + .padding(.trailing, 20) + .padding(.bottom, 20) + } + } + } + } + .navigationTitle(state.displayName) + .navigationDestination(item: Binding( + get: { selectedBookmarkId }, + set: { selectedBookmarkId = $0 } + )) { bookmarkId in + BookmarkDetailView(bookmarkId: bookmarkId) } - .navigationTitle(state.displayName) .sheet(isPresented: $showingAddBookmark) { AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle) } @@ -79,7 +111,7 @@ struct BookmarksView: View { // Refresh bookmarks when sheet is dismissed if oldValue && !newValue { Task { - await viewModel.refreshBookmarks() + await viewModel.loadBookmarks(state: state) } } } @@ -87,32 +119,6 @@ struct BookmarksView: View { handleShareNotification(notification) } } - .overlay { - // Animated FAB Button - VStack { - Spacer() - HStack { - Spacer() - - Button(action: { - showingAddBookmark = true - }) { - Image(systemName: "plus") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.white) - .frame(width: 56, height: 56) - .background(Color.accentColor) - .clipShape(Circle()) - .shadow(color: .black.opacity(0.25), radius: 6, x: 0, y: 3) - .scaleEffect(isScrolling ? 0.8 : 1.0) - .opacity(isScrolling ? 0.7 : 1.0) - } - .padding(.trailing, 20) - .padding(.bottom, 20) - } - } - } } private func handleShareNotification(_ notification: Notification) { @@ -130,10 +136,7 @@ struct BookmarksView: View { } } -// Helper für Scroll-Tracking -struct ScrollOffsetPreferenceKey: PreferenceKey { - static var defaultValue: CGFloat = 0 - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value = nextValue() - } +// String Identifiable Extension für navigationDestination +extension String: Identifiable { + public var id: String { self } } diff --git a/readeck/Utils/SafariUtil.swift b/readeck/Utils/SafariUtil.swift new file mode 100644 index 0000000..f1c9ff8 --- /dev/null +++ b/readeck/Utils/SafariUtil.swift @@ -0,0 +1,25 @@ +import UIKit +import SafariServices + +class SafariUtil { + static func openInSafari(url: String) { + guard let url = URL(string: url) else { return } + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first, + let rootViewController = window.rootViewController { + + let safariViewController = SFSafariViewController(url: url) + safariViewController.preferredBarTintColor = UIColor.systemBackground + safariViewController.preferredControlTintColor = UIColor.tintColor + + // Finde den präsentierenden View Controller + var presentingViewController = rootViewController + while let presented = presentingViewController.presentedViewController { + presentingViewController = presented + } + + presentingViewController.present(safariViewController, animated: true) + } + } +}