refactor: Simplify Share Extension to open main app directly
- Refactor ShareViewController to extract URL and open main app instead of direct API calls - Add robust URL extraction from multiple content types (URL, text, property lists) - Implement comprehensive debugging for Share Extension content processing - Add URL scheme handling in main app (readeck://add-bookmark) - Add notification-based communication between Share Extension and main app - Extend BookmarksViewModel with share notification handling - Support automatic AddBookmarkView opening with prefilled URL and title from shares Technical changes: - Remove Core Data dependency from Share Extension - Add extensionContext.open() for launching main app with custom URL scheme - Process all registered type identifiers for robust content extraction - Add NSDataDetector for URL extraction from plain text - Handle Safari property list sharing format - Add share state management in BookmarksViewModel (@Observable pattern) - Implement NotificationCenter publisher pattern with Combine URL scheme format: readeck://add-bookmark?url=...&title=... Notification: 'AddBookmarkFromShare' with url and title in userInfo
This commit is contained in:
parent
82f9d8a5a9
commit
8882a402ef
@ -8,188 +8,59 @@
|
||||
import UIKit
|
||||
import Social
|
||||
import UniformTypeIdentifiers
|
||||
import CoreData
|
||||
|
||||
class ShareViewController: SLComposeServiceViewController {
|
||||
|
||||
private var extractedURL: String?
|
||||
private var extractedTitle: String?
|
||||
private var isProcessing = false
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
extractSharedContent()
|
||||
setupUI()
|
||||
|
||||
// Automatisch die Haupt-App öffnen, sobald URL extrahiert wurde
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.openParentApp()
|
||||
}
|
||||
}
|
||||
|
||||
override func isContentValid() -> Bool {
|
||||
guard let url = extractedURL,
|
||||
!url.isEmpty,
|
||||
!isProcessing,
|
||||
URL(string: url) != nil else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return true // Immer true, damit der Button funktioniert
|
||||
}
|
||||
|
||||
override func didSelectPost() {
|
||||
guard let url = extractedURL else {
|
||||
completeRequest()
|
||||
return
|
||||
}
|
||||
|
||||
isProcessing = true
|
||||
let title = textView.text != extractedTitle ? textView.text : extractedTitle
|
||||
|
||||
// UI Feedback zeigen
|
||||
let loadingAlert = UIAlertController(title: "Speichere Bookmark", message: "Bitte warten...", preferredStyle: .alert)
|
||||
present(loadingAlert, animated: true)
|
||||
|
||||
Task {
|
||||
do {
|
||||
let response = try await createBookmark(url: url, title: title)
|
||||
|
||||
await MainActor.run {
|
||||
loadingAlert.dismiss(animated: true)
|
||||
|
||||
if response.status == 0 {
|
||||
// Erfolg
|
||||
let successAlert = UIAlertController(title: "Erfolg", message: "Bookmark wurde hinzugefügt!", preferredStyle: .alert)
|
||||
successAlert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
|
||||
self?.completeRequest()
|
||||
})
|
||||
present(successAlert, animated: true)
|
||||
} else {
|
||||
// Fehler vom Server
|
||||
let errorAlert = UIAlertController(title: "Fehler", message: response.message, preferredStyle: .alert)
|
||||
errorAlert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
|
||||
self?.completeRequest()
|
||||
})
|
||||
present(errorAlert, animated: true)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
loadingAlert.dismiss(animated: true)
|
||||
let errorAlert = UIAlertController(title: "Fehler", message: error.localizedDescription, preferredStyle: .alert)
|
||||
errorAlert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
|
||||
self?.completeRequest()
|
||||
})
|
||||
present(errorAlert, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
openMainApp()
|
||||
}
|
||||
|
||||
// MARK: - Core Data Token Retrieval
|
||||
|
||||
private func getCurrentToken() -> String? {
|
||||
let context = CoreDataManager.shared.context
|
||||
let request: NSFetchRequest<SettingEntity> = SettingEntity.fetchRequest() // Anpassen an deine Entity
|
||||
|
||||
do {
|
||||
let tokens = try context.fetch(request)
|
||||
return tokens.first?.token // Anpassen an dein Token-Attribut
|
||||
} catch {
|
||||
print("Failed to fetch token from Core Data: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - API Implementation
|
||||
|
||||
private func createBookmark(url: String, title: String?) async throws -> CreateBookmarkResponseDto {
|
||||
let requestDto = CreateBookmarkRequestDto(labels: nil, title: title, url: url)
|
||||
|
||||
// Die Server-URL
|
||||
let baseURL = "https://keep.mnk.any64.de"
|
||||
let endpoint = "/api/bookmarks"
|
||||
|
||||
guard let requestURL = URL(string: "\(baseURL)\(endpoint)") else {
|
||||
throw RequestError.invalidURL
|
||||
}
|
||||
|
||||
var request = URLRequest(url: requestURL)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
|
||||
// Token aus Core Data holen
|
||||
guard let token = getCurrentToken() else {
|
||||
throw RequestError.noToken
|
||||
}
|
||||
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
request.httpBody = try encoder.encode(requestDto)
|
||||
|
||||
// Request durchführen
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw RequestError.invalidResponse
|
||||
}
|
||||
|
||||
guard 200...299 ~= httpResponse.statusCode else {
|
||||
throw RequestError.serverError(statusCode: httpResponse.statusCode)
|
||||
}
|
||||
|
||||
// Response dekodieren und zurückgeben
|
||||
let decoder = JSONDecoder()
|
||||
return try decoder.decode(CreateBookmarkResponseDto.self, from: data)
|
||||
}
|
||||
|
||||
// MARK: - Support Methods
|
||||
|
||||
private func setupUI() {
|
||||
self.title = "Zu readeck hinzufügen"
|
||||
self.placeholder = "Optional: Titel anpassen..."
|
||||
|
||||
// Zeige URL oder Titel als Kontext
|
||||
if let title = extractedTitle {
|
||||
self.textView.text = title
|
||||
} else if let url = extractedURL {
|
||||
self.textView.text = URL(string: url)?.host ?? url
|
||||
}
|
||||
}
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func extractSharedContent() {
|
||||
guard let extensionContext = extensionContext else { return }
|
||||
|
||||
for item in extensionContext.inputItems {
|
||||
print("=== DEBUG: Starting content extraction ===")
|
||||
print("Input items count: \(extensionContext.inputItems.count)")
|
||||
|
||||
for (itemIndex, item) in extensionContext.inputItems.enumerated() {
|
||||
guard let inputItem = item as? NSExtensionItem else { continue }
|
||||
|
||||
for provider in inputItem.attachments ?? [] {
|
||||
// URL direkt
|
||||
if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
|
||||
provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] item, error in
|
||||
if let url = item as? URL {
|
||||
DispatchQueue.main.async {
|
||||
self?.extractedURL = url.absoluteString
|
||||
self?.extractedTitle = inputItem.attributedTitle?.string ?? inputItem.attributedContentText?.string
|
||||
self?.setupUI()
|
||||
}
|
||||
print("Item \(itemIndex) - attachments: \(inputItem.attachments?.count ?? 0)")
|
||||
|
||||
// Versuche alle verfügbaren Type Identifiers
|
||||
for (attachmentIndex, provider) in (inputItem.attachments ?? []).enumerated() {
|
||||
print("Attachment \(attachmentIndex) - registered types: \(provider.registeredTypeIdentifiers)")
|
||||
|
||||
// Iteriere durch alle registrierten Type Identifiers
|
||||
for typeIdentifier in provider.registeredTypeIdentifiers {
|
||||
print("Trying type identifier: \(typeIdentifier)")
|
||||
|
||||
provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { [weak self] item, error in
|
||||
if let error = error {
|
||||
print("Error loading \(typeIdentifier): \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// Text (könnte URL enthalten)
|
||||
else if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
|
||||
provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { [weak self] item, error in
|
||||
if let text = item as? String {
|
||||
DispatchQueue.main.async {
|
||||
self?.handleTextContent(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Property List (Safari teilt so)
|
||||
else if provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) {
|
||||
provider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil) { [weak self] item, error in
|
||||
if let dictionary = item as? [String: Any] {
|
||||
DispatchQueue.main.async {
|
||||
self?.handlePropertyList(dictionary)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self?.processLoadedItem(item, typeIdentifier: typeIdentifier, inputItem: inputItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -197,72 +68,156 @@ class ShareViewController: SLComposeServiceViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleTextContent(_ text: String) {
|
||||
private func processLoadedItem(_ item: Any?, typeIdentifier: String, inputItem: NSExtensionItem) {
|
||||
print("Processing item of type \(typeIdentifier): \(type(of: item))")
|
||||
|
||||
// URL direkt
|
||||
if let url = item as? URL {
|
||||
print("Found URL: \(url.absoluteString)")
|
||||
extractedURL = url.absoluteString
|
||||
extractedTitle = inputItem.attributedTitle?.string ?? inputItem.attributedContentText?.string
|
||||
return
|
||||
}
|
||||
|
||||
// NSURL
|
||||
if let nsurl = item as? NSURL {
|
||||
print("Found NSURL: \(nsurl.absoluteString ?? "nil")")
|
||||
extractedURL = nsurl.absoluteString
|
||||
extractedTitle = inputItem.attributedTitle?.string ?? inputItem.attributedContentText?.string
|
||||
return
|
||||
}
|
||||
|
||||
// String (könnte URL sein)
|
||||
if let text = item as? String {
|
||||
print("Found String: \(text)")
|
||||
if URL(string: text) != nil {
|
||||
extractedURL = text
|
||||
extractedTitle = inputItem.attributedTitle?.string ?? inputItem.attributedContentText?.string
|
||||
return
|
||||
}
|
||||
|
||||
// Versuche URL aus Text zu extrahieren
|
||||
if let extractedURL = extractURLFromText(text) {
|
||||
self.extractedURL = extractedURL
|
||||
self.extractedTitle = text != extractedURL ? text : nil
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Dictionary (Property List)
|
||||
if let dictionary = item as? [String: Any] {
|
||||
print("Found Dictionary: \(dictionary)")
|
||||
handlePropertyList(dictionary)
|
||||
return
|
||||
}
|
||||
|
||||
// NSData - versuche als String zu interpretieren
|
||||
if let data = item as? Data {
|
||||
if let text = String(data: data, encoding: .utf8) {
|
||||
print("Found Data as String: \(text)")
|
||||
if URL(string: text) != nil {
|
||||
extractedURL = text
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print("Could not process item of type: \(type(of: item))")
|
||||
}
|
||||
|
||||
private func extractURLFromText(_ text: String) -> String? {
|
||||
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||
let range = NSRange(text.startIndex..<text.endIndex, in: text)
|
||||
|
||||
if let match = detector?.firstMatch(in: text, options: [], range: range),
|
||||
let url = match.url {
|
||||
extractedURL = url.absoluteString
|
||||
extractedTitle = text != url.absoluteString ? text : nil
|
||||
} else if URL(string: text) != nil {
|
||||
extractedURL = text
|
||||
return url.absoluteString
|
||||
}
|
||||
|
||||
setupUI()
|
||||
return nil
|
||||
}
|
||||
|
||||
private func handlePropertyList(_ dictionary: [String: Any]) {
|
||||
if let urlString = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any],
|
||||
let url = urlString["URL"] as? String {
|
||||
extractedURL = url
|
||||
extractedTitle = urlString["title"] as? String
|
||||
} else if let url = dictionary["URL"] as? String {
|
||||
extractedURL = url
|
||||
extractedTitle = dictionary["title"] as? String
|
||||
// Safari und andere Browser verwenden oft Property Lists
|
||||
if let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any] {
|
||||
if let url = results["URL"] as? String {
|
||||
extractedURL = url
|
||||
extractedTitle = results["title"] as? String
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setupUI()
|
||||
// Direkte URL im Dictionary
|
||||
if let url = dictionary["URL"] as? String {
|
||||
extractedURL = url
|
||||
extractedTitle = dictionary["title"] as? String
|
||||
return
|
||||
}
|
||||
|
||||
// Andere mögliche Keys
|
||||
for key in dictionary.keys {
|
||||
if let value = dictionary[key] as? String, URL(string: value) != nil {
|
||||
extractedURL = value
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openMainApp() {
|
||||
let url = extractedURL ?? "https://example.com"
|
||||
let title = extractedTitle ?? ""
|
||||
|
||||
print("Opening main app with URL: \(url)")
|
||||
|
||||
// Verwende NSUserActivity anstatt URL-Schema
|
||||
let userActivity = NSUserActivity(activityType: "de.ilyas.readeck")
|
||||
userActivity.userInfo = [
|
||||
"url": url,
|
||||
"title": title
|
||||
]
|
||||
userActivity.webpageURL = URL(string: url)
|
||||
|
||||
// Extension schließen und Activity übergeben
|
||||
extensionContext?.completeRequest(returningItems: [userActivity], completionHandler: nil)
|
||||
}
|
||||
|
||||
private func completeRequest() {
|
||||
isProcessing = false
|
||||
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DTOs und Error Handling
|
||||
|
||||
struct CreateBookmarkRequestDto: Codable {
|
||||
let labels: [String]?
|
||||
let title: String?
|
||||
let url: String
|
||||
}
|
||||
|
||||
struct CreateBookmarkResponseDto: Codable {
|
||||
let message: String
|
||||
let status: Int
|
||||
}
|
||||
|
||||
enum RequestError: Error, LocalizedError {
|
||||
case invalidURL
|
||||
case invalidResponse
|
||||
case serverError(statusCode: Int)
|
||||
case decodingError
|
||||
case noToken
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidURL:
|
||||
return "Ungültige URL"
|
||||
case .invalidResponse:
|
||||
return "Ungültige Server-Antwort"
|
||||
case .serverError(let code):
|
||||
return "Server-Fehler: \(code)"
|
||||
case .decodingError:
|
||||
return "Fehler beim Dekodieren der Antwort"
|
||||
case .noToken:
|
||||
return "Kein Authentifizierungs-Token gefunden"
|
||||
func openParentApp() {
|
||||
guard let extensionContext = extensionContext else { return }
|
||||
|
||||
let url = extractedURL ?? "https://example.com"
|
||||
let title = extractedTitle ?? ""
|
||||
|
||||
print("Opening parent app with URL: \(url)")
|
||||
|
||||
// URL für die Haupt-App erstellen mit Parametern
|
||||
var urlComponents = URLComponents(string: "readeck://add-bookmark")
|
||||
urlComponents?.queryItems = [
|
||||
URLQueryItem(name: "url", value: url)
|
||||
]
|
||||
|
||||
if !title.isEmpty {
|
||||
urlComponents?.queryItems?.append(URLQueryItem(name: "title", value: title))
|
||||
}
|
||||
|
||||
guard let finalURL = urlComponents?.url else {
|
||||
print("Failed to create final URL")
|
||||
return
|
||||
}
|
||||
|
||||
print("Final URL: \(finalURL)")
|
||||
|
||||
var responder: UIResponder? = self
|
||||
|
||||
while responder != nil {
|
||||
if let application = responder as? UIApplication {
|
||||
application.open(finalURL)
|
||||
break
|
||||
}
|
||||
responder = responder?.next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,11 @@ struct AddBookmarkView: View {
|
||||
@State private var viewModel = AddBookmarkViewModel()
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
init(prefilledURL: String? = nil, prefilledTitle: String? = nil) {
|
||||
viewModel.title = prefilledTitle ?? ""
|
||||
viewModel.url = prefilledURL ?? ""
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
@ -78,6 +83,7 @@ struct AddBookmarkView: View {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Abbrechen") {
|
||||
dismiss()
|
||||
viewModel.clearForm()
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,6 +91,7 @@ struct AddBookmarkView: View {
|
||||
Button("Speichern") {
|
||||
Task {
|
||||
await viewModel.createBookmark()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.disabled(!viewModel.isValid || viewModel.isLoading)
|
||||
@ -126,9 +133,12 @@ struct AddBookmarkView: View {
|
||||
.onAppear {
|
||||
viewModel.checkClipboard()
|
||||
}
|
||||
.onDisappear {
|
||||
viewModel.clearForm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AddBookmarkView()
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,8 +50,7 @@ class AddBookmarkViewModel {
|
||||
// Optional: Zeige die Server-Nachricht an
|
||||
print("Server response: \(message)")
|
||||
|
||||
showSuccessAlert = true
|
||||
|
||||
clearForm()
|
||||
} catch let error as CreateBookmarkError {
|
||||
errorMessage = error.localizedDescription
|
||||
showErrorAlert = true
|
||||
@ -77,4 +76,10 @@ class AddBookmarkViewModel {
|
||||
guard let clipboardURL = clipboardURL else { return }
|
||||
url = clipboardURL
|
||||
}
|
||||
}
|
||||
|
||||
func clearForm() {
|
||||
url = ""
|
||||
title = ""
|
||||
labelsText = ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,10 @@ struct BookmarksView: View {
|
||||
@State private var isScrolling = false
|
||||
let state: BookmarkState
|
||||
|
||||
@State private var showingAddBookmarkFromShare = false
|
||||
@State private var shareURL = ""
|
||||
@State private var shareTitle = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
@ -59,7 +63,7 @@ struct BookmarksView: View {
|
||||
}
|
||||
.navigationTitle(state.displayName)
|
||||
.sheet(isPresented: $showingAddBookmark) {
|
||||
AddBookmarkView()
|
||||
AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle)
|
||||
}
|
||||
.alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) {
|
||||
Button("OK", role: .cancel) {
|
||||
@ -79,6 +83,9 @@ struct BookmarksView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("AddBookmarkFromShare"))) { notification in
|
||||
handleShareNotification(notification)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
// Animated FAB Button
|
||||
@ -107,6 +114,20 @@ struct BookmarksView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleShareNotification(_ notification: Notification) {
|
||||
guard let userInfo = notification.userInfo,
|
||||
let url = userInfo["url"] as? String,
|
||||
!url.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
shareURL = url
|
||||
shareTitle = userInfo["title"] as? String ?? ""
|
||||
showingAddBookmark = true
|
||||
|
||||
print("Received share notification - URL: \(url), Title: \(shareTitle)")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper für Scroll-Tracking
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@Observable
|
||||
class BookmarksViewModel {
|
||||
@ -10,10 +11,40 @@ class BookmarksViewModel {
|
||||
var isLoading = false
|
||||
var errorMessage: String?
|
||||
var currentState: BookmarkState = .unread
|
||||
|
||||
|
||||
var showingAddBookmarkFromShare = false
|
||||
var shareURL = ""
|
||||
var shareTitle = ""
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init() {
|
||||
|
||||
setupNotificationObserver()
|
||||
}
|
||||
|
||||
private func setupNotificationObserver() {
|
||||
NotificationCenter.default
|
||||
.publisher(for: NSNotification.Name("AddBookmarkFromShare"))
|
||||
.sink { [weak self] notification in
|
||||
self?.handleShareNotification(notification)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func handleShareNotification(_ notification: Notification) {
|
||||
guard let userInfo = notification.userInfo,
|
||||
let url = userInfo["url"] as? String,
|
||||
!url.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.shareURL = url
|
||||
self.shareTitle = userInfo["title"] as? String ?? ""
|
||||
self.showingAddBookmarkFromShare = true
|
||||
}
|
||||
|
||||
print("Received share notification - URL: \(url)")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
enum BookmarkState: String, CaseIterable {
|
||||
enum BookmarkState: String, CaseIterable {
|
||||
case unread = "unread"
|
||||
case favorite = "favorite"
|
||||
case archived = "archived"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user