iOS: gescannter Einzelstück-Link öffnet wirklich das Stück

Universal Link oeffnete die App, sprang aber nicht aufs Stueck. Jetzt robuster:
- Kaltstart wird abgefangen (AppDelegate application(_:continue:) + pending-URL),
  zusaetzlich zu den SwiftUI-Hooks.
- Beim Link wird auf das Server-Profil gewechselt, dessen Adresse zum Host passt
  (sonst wuerde die UID am falschen Server gesucht).
- Schlaegt das Aufloesen fehl (nicht angemeldet, UID unbekannt), erscheint jetzt
  eine Meldung statt stiller Nichtreaktion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-26 10:24:05 +02:00
parent b4b203a117
commit 2d7cfa7fcb
3 changed files with 49 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ struct RootView: View {
@EnvironmentObject private var display: DisplaySettings @EnvironmentObject private var display: DisplaySettings
/// Über einen gescannten Universal Link geöffnetes Einzelstück. /// Über einen gescannten Universal Link geöffnetes Einzelstück.
@State private var linkedItem: Item? @State private var linkedItem: Item?
@State private var linkError: String?
var body: some View { var body: some View {
Group { Group {
@@ -45,12 +46,25 @@ struct RootView: View {
} }
.task(id: router.openItemUid) { .task(id: router.openItemUid) {
guard let uid = router.openItemUid else { return } guard let uid = router.openItemUid else { return }
linkedItem = try? await APIClient.shared.itemByUid(uid: uid)
router.openItemUid = nil router.openItemUid = nil
guard session.isLoggedIn else {
linkError = "Bitte zuerst am passenden Server anmelden, dann erneut scannen."
return
}
do {
linkedItem = try await APIClient.shared.itemByUid(uid: uid)
} catch {
linkError = "Einzelstück \(uid) nicht gefunden: \(error.localizedDescription)"
}
} }
.sheet(item: $linkedItem) { it in .sheet(item: $linkedItem) { it in
NavigationStack { ItemEditView(item: it) } NavigationStack { ItemEditView(item: it) }
} }
.alert("Einzelstück öffnen", isPresented: Binding(
get: { linkError != nil }, set: { if !$0 { linkError = nil } }
)) {
Button("OK", role: .cancel) { linkError = nil }
} message: { Text(linkError ?? "") }
} }
} }

View File

@@ -140,6 +140,17 @@ final class Session: ObservableObject {
loadTokenForActiveProfile() loadTokenForActiveProfile()
} }
/// Wechselt (falls vorhanden und noetig) auf das Profil, dessen Adresse zum
/// Host eines Links passt damit ein gescannter QR den richtigen Server
/// abfragt, auch wenn gerade ein anderer aktiv ist.
func switchToProfile(matching url: URL) {
guard let host = url.host?.lowercased() else { return }
if activeProfile?.url?.host?.lowercased() == host { return }
if let match = profiles.first(where: { $0.url?.host?.lowercased() == host }) {
switchTo(match.id)
}
}
// MARK: - Anmeldung // MARK: - Anmeldung
/// Legt die Adresse fuer die Anmeldung fest: aktualisiert das aktive Profil /// Legt die Adresse fuer die Anmeldung fest: aktualisiert das aktive Profil

View File

@@ -40,6 +40,8 @@ final class Router: ObservableObject {
} }
let parts = url.pathComponents // z.B. ["/", "i", "ABC123"] let parts = url.pathComponents // z.B. ["/", "i", "ABC123"]
if let idx = parts.firstIndex(of: "i"), idx + 1 < parts.count { if let idx = parts.firstIndex(of: "i"), idx + 1 < parts.count {
// Auf den Server aus dem Link wechseln, damit die UID dort gesucht wird.
Session.shared.switchToProfile(matching: url)
openItemUid = parts[idx + 1].uppercased() openItemUid = parts[idx + 1].uppercased()
} }
} }
@@ -73,6 +75,11 @@ struct VorraniaApp: App {
router.handle(expiryProfile: profile) router.handle(expiryProfile: profile)
AppDelegate.pendingExpiryProfile = nil AppDelegate.pendingExpiryProfile = nil
} }
// Kaltstart aus einem gescannten Universal Link.
if let url = AppDelegate.pendingURL {
router.handle(url: url)
AppDelegate.pendingURL = nil
}
} }
.onReceive(NotificationCenter.default.publisher(for: AppDelegate.shortcutNotification)) { note in .onReceive(NotificationCenter.default.publisher(for: AppDelegate.shortcutNotification)) { note in
if let type = note.object as? String { router.handle(shortcut: type) } if let type = note.object as? String { router.handle(shortcut: type) }
@@ -80,6 +87,9 @@ struct VorraniaApp: App {
.onReceive(NotificationCenter.default.publisher(for: AppDelegate.expiryTapNotification)) { note in .onReceive(NotificationCenter.default.publisher(for: AppDelegate.expiryTapNotification)) { note in
if let profile = note.object as? String { router.handle(expiryProfile: profile) } if let profile = note.object as? String { router.handle(expiryProfile: profile) }
} }
.onReceive(NotificationCenter.default.publisher(for: AppDelegate.urlNotification)) { note in
if let url = note.object as? URL { router.handle(url: url) }
}
} }
// Im Vordergrund den Meldungsplan auffrischen und den naechsten // Im Vordergrund den Meldungsplan auffrischen und den naechsten
// Hintergrundlauf einplanen. // Hintergrundlauf einplanen.
@@ -101,8 +111,10 @@ struct VorraniaApp: App {
final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
static let shortcutNotification = Notification.Name("VorraniaShortcut") static let shortcutNotification = Notification.Name("VorraniaShortcut")
static let expiryTapNotification = Notification.Name("VorraniaExpiryTap") static let expiryTapNotification = Notification.Name("VorraniaExpiryTap")
static let urlNotification = Notification.Name("VorraniaOpenURL")
static var pendingShortcut: String? static var pendingShortcut: String?
static var pendingExpiryProfile: String? static var pendingExpiryProfile: String?
static var pendingURL: URL?
static let refreshTaskID = "com.scarriffle.vorrania.refresh" static let refreshTaskID = "com.scarriffle.vorrania.refresh"
@@ -132,6 +144,17 @@ final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCent
completionHandler(true) completionHandler(true)
} }
// Universal Link (/i/<UID>) auch beim Kaltstart, falls die SwiftUI-Hooks
// ihn nicht mitbekommen.
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else { return false }
AppDelegate.pendingURL = url
NotificationCenter.default.post(name: AppDelegate.urlNotification, object: url)
return true
}
// MARK: - Benachrichtigungen // MARK: - Benachrichtigungen
/// Auch im Vordergrund als Banner zeigen - sonst bliebe eine faellige /// Auch im Vordergrund als Banner zeigen - sonst bliebe eine faellige