From 209cf6caf57aa30205825adeb1784639f9df4c06 Mon Sep 17 00:00:00 2001 From: Greg Bolsinga Date: Sun, 29 Sep 2024 22:17:25 -0700 Subject: [PATCH] Add ArchivePath userActivity to ArchiveNavigationUserActivityModifier - it's all based upon ArchiveNavigation and sits "at the top". - less data to pipe down to lower views. - basically the other day I realized that .userActivity was just sharing the current ArchiveNavigation state! --- .../Music/ArchivePath+PathRestorable.swift | 4 +- Sources/Site/Music/UI/ArchiveNavigation.swift | 4 ++ ...rchiveNavigationUserActivityModifier.swift | 45 ++++++++++++++++-- Sources/Site/Music/UI/ArchiveStateView.swift | 3 +- Sources/Site/Music/UI/ArtistDetail.swift | 13 +---- .../Music/UI/MusicDestinationModifier.swift | 18 +++---- .../PathRestorableUserActivityModifier.swift | 47 ------------------- Sources/Site/Music/UI/ShowDetail.swift | 11 ----- Sources/Site/Music/UI/VenueDetail.swift | 5 -- Sources/Site/Music/UI/YearDetail.swift | 5 -- Sources/Site/Music/Vault+ArchivePath.swift | 23 +++++++++ Tests/SiteTests/ArchiveNavigationTests.swift | 15 ++++++ 12 files changed, 93 insertions(+), 100 deletions(-) delete mode 100644 Sources/Site/Music/UI/PathRestorableUserActivityModifier.swift create mode 100644 Sources/Site/Music/Vault+ArchivePath.swift diff --git a/Sources/Site/Music/ArchivePath+PathRestorable.swift b/Sources/Site/Music/ArchivePath+PathRestorable.swift index 1339cb57..065a6d23 100644 --- a/Sources/Site/Music/ArchivePath+PathRestorable.swift +++ b/Sources/Site/Music/ArchivePath+PathRestorable.swift @@ -14,7 +14,7 @@ extension Array where Element == ArchivePath { return !self.dropLast().contains { $0 == archivePath } } - func isPathActive(_ pathRestorable: PathRestorable) -> Bool { - self.last == pathRestorable.archivePath + func isPathActive(_ archivePath: ArchivePath) -> Bool { + self.last == archivePath } } diff --git a/Sources/Site/Music/UI/ArchiveNavigation.swift b/Sources/Site/Music/UI/ArchiveNavigation.swift index 4f99ca49..200d717d 100644 --- a/Sources/Site/Music/UI/ArchiveNavigation.swift +++ b/Sources/Site/Music/UI/ArchiveNavigation.swift @@ -94,6 +94,10 @@ extension Logger { // category is active if its path is empty return path.isEmpty } + + func userActivityActive(for path: ArchivePath) -> Bool { + self.path.isPathActive(path) + } } extension ArchiveNavigation: RawRepresentable { diff --git a/Sources/Site/Music/UI/ArchiveNavigationUserActivityModifier.swift b/Sources/Site/Music/UI/ArchiveNavigationUserActivityModifier.swift index f5ab12a9..41c1e367 100644 --- a/Sources/Site/Music/UI/ArchiveNavigationUserActivityModifier.swift +++ b/Sources/Site/Music/UI/ArchiveNavigationUserActivityModifier.swift @@ -12,16 +12,27 @@ extension ArchiveCategory { static let activityType = "gdb.SiteApp.view-archiveCategory" } +extension ArchivePath { + static let activityType = "gdb.SiteApp.view-archivePath" +} + extension Logger { fileprivate static let navigationUserActivity = Logger(category: "navigationUserActivity") } +protocol PathRestorableUserActivity: PathRestorable { + var url: URL? { get } + func updateActivity(_ userActivity: NSUserActivity) +} + struct ArchiveNavigationUserActivityModifier: ViewModifier { let archiveNavigation: ArchiveNavigation let urlForCategory: (ArchiveCategory) -> URL? + let activityForPath: (ArchivePath) -> PathRestorableUserActivity? @State private var userActivityCategory: ArchiveNavigation.State.DefaultCategory = ArchiveNavigation.State.defaultCategory + @State private var userActivityPath: [ArchivePath] = [] func body(content: Content) -> some View { let isCategoryActive = { @@ -31,37 +42,61 @@ struct ArchiveNavigationUserActivityModifier: ViewModifier { return archiveNavigation.userActivityActive(for: userActivityCategory) }() + let isPathActive = + !isCategoryActive + && { + guard let lastPath = userActivityPath.last else { return false } + return archiveNavigation.userActivityActive(for: lastPath) + }() + #if os(iOS) || os(tvOS) Logger.navigationUserActivity.log( - "\(userActivityCategory?.rawValue ?? "nil", privacy: .public) active: \(isCategoryActive, privacy: .public)" + "\(userActivityCategory?.rawValue ?? "nil", privacy: .public): \(isCategoryActive, privacy: .public) \(userActivityPath, privacy: .public): \(isPathActive)" ) #elseif os(macOS) Logger.navigationUserActivity.log( - "\(userActivityCategory.rawValue, privacy: .public) active: \(isCategoryActive, privacy: .public)" + "\(userActivityCategory.rawValue, privacy: .public): \(isCategoryActive, privacy: .public) \(userActivityPath, privacy: .public): \(isPathActive)" ) #endif return content - .onAppear { userActivityCategory = archiveNavigation.category } + .onAppear { + userActivityCategory = archiveNavigation.category + userActivityPath = archiveNavigation.path + } .onChange(of: archiveNavigation.category) { _, newValue in userActivityCategory = newValue } + .onChange(of: archiveNavigation.path) { _, newValue in + userActivityPath = newValue + } .userActivity(ArchiveCategory.activityType, isActive: isCategoryActive) { userActivity in #if os(iOS) || os(tvOS) guard let userActivityCategory else { return } #endif + Logger.navigationUserActivity.log( + "update category \(userActivityCategory.rawValue, privacy: .public)") userActivity.update(userActivityCategory, url: urlForCategory(userActivityCategory)) } + .userActivity(ArchivePath.activityType, isActive: isPathActive) { userActivity in + guard let lastPath = userActivityPath.last else { return } + Logger.navigationUserActivity.log( + "update path \(lastPath.formatted(.json), privacy: .public)") + guard let pathUserActivity = activityForPath(lastPath) else { return } + userActivity.update(pathUserActivity) + } } } extension View { func advertiseUserActivity( - for archiveNavigation: ArchiveNavigation, urlForCategory: @escaping (ArchiveCategory) -> URL? + for archiveNavigation: ArchiveNavigation, urlForCategory: @escaping (ArchiveCategory) -> URL?, + activityForPath: @escaping (ArchivePath) -> PathRestorableUserActivity? ) -> some View { modifier( ArchiveNavigationUserActivityModifier( - archiveNavigation: archiveNavigation, urlForCategory: urlForCategory)) + archiveNavigation: archiveNavigation, urlForCategory: urlForCategory, + activityForPath: activityForPath)) } } diff --git a/Sources/Site/Music/UI/ArchiveStateView.swift b/Sources/Site/Music/UI/ArchiveStateView.swift index 34245078..2fba674a 100644 --- a/Sources/Site/Music/UI/ArchiveStateView.swift +++ b/Sources/Site/Music/UI/ArchiveStateView.swift @@ -70,7 +70,8 @@ struct ArchiveStateView: View { } } .advertiseUserActivity( - for: archiveNavigation, urlForCategory: { model.vault.categoryURLMap[$0] }) + for: archiveNavigation, urlForCategory: { model.vault.categoryURLMap[$0] } + ) { model.vault.pathUserActivity(for: $0) } } } diff --git a/Sources/Site/Music/UI/ArtistDetail.swift b/Sources/Site/Music/UI/ArtistDetail.swift index 25f08af0..0c8dddbd 100644 --- a/Sources/Site/Music/UI/ArtistDetail.swift +++ b/Sources/Site/Music/UI/ArtistDetail.swift @@ -11,7 +11,6 @@ struct ArtistDetail: View { let digest: ArtistDigest let concertCompare: (Concert, Concert) -> Bool let isPathNavigable: (PathRestorable) -> Bool - let isPathActive: (PathRestorable) -> Bool @ViewBuilder private var firstSetElement: some View { HStack { @@ -72,7 +71,6 @@ struct ArtistDetail: View { .listStyle(.grouped) #endif .navigationTitle(digest.artist.name) - .pathRestorableUserActivityModifier(digest, isPathActive: isPathActive) .archiveShare(digest.artist, url: digest.url) } } @@ -84,9 +82,6 @@ struct ArtistDetail: View { concertCompare: vaultPreviewData.comparator.compare(lhs:rhs:), isPathNavigable: { _ in true - }, - isPathActive: { _ in - true } ) .musicDestinations(vaultPreviewData) @@ -100,9 +95,6 @@ struct ArtistDetail: View { concertCompare: vaultPreviewData.comparator.compare(lhs:rhs:), isPathNavigable: { _ in true - }, - isPathActive: { _ in - true } ) .musicDestinations(vaultPreviewData) @@ -115,10 +107,7 @@ struct ArtistDetail: View { ArtistDetail( digest: vaultPreviewData.artistDigests[1], concertCompare: vaultPreviewData.comparator.compare(lhs:rhs:), - isPathNavigable: { $0.archivePath != selectedConcert.archivePath }, - isPathActive: { _ in - true - } + isPathNavigable: { $0.archivePath != selectedConcert.archivePath } ) .musicDestinations(vaultPreviewData, path: [selectedConcert.archivePath]) } diff --git a/Sources/Site/Music/UI/MusicDestinationModifier.swift b/Sources/Site/Music/UI/MusicDestinationModifier.swift index 30cc9f42..7a06a8f5 100644 --- a/Sources/Site/Music/UI/MusicDestinationModifier.swift +++ b/Sources/Site/Music/UI/MusicDestinationModifier.swift @@ -11,7 +11,6 @@ import SwiftUI struct MusicDestinationModifier: ViewModifier { let vault: Vault let isPathNavigable: (PathRestorable) -> Bool - let isPathActive: (PathRestorable) -> Bool func body(content: Content) -> some View { content @@ -19,27 +18,25 @@ struct MusicDestinationModifier: ViewModifier { switch archivePath { case .show(let iD): if let concert = vault.concertMap[iD] { - ShowDetail( - concert: concert, isPathNavigable: isPathNavigable, isPathActive: isPathActive) + ShowDetail(concert: concert, isPathNavigable: isPathNavigable) } case .venue(let iD): if let venueDigest = vault.venueDigestMap[iD] { VenueDetail( digest: venueDigest, concertCompare: vault.comparator.compare(lhs:rhs:), - geocode: { - try await vault.atlas.geocode($0.venue) - }, isPathNavigable: isPathNavigable, isPathActive: isPathActive) + geocode: { try await vault.atlas.geocode($0.venue) }, isPathNavigable: isPathNavigable + ) } case .artist(let iD): if let artistDigest = vault.artistDigestMap[iD] { ArtistDetail( digest: artistDigest, concertCompare: vault.comparator.compare(lhs:rhs:), - isPathNavigable: isPathNavigable, isPathActive: isPathActive) + isPathNavigable: isPathNavigable) } case .year(let annum): YearDetail( digest: vault.digest(for: annum), concertCompare: vault.comparator.compare(lhs:rhs:), - isPathNavigable: isPathNavigable, isPathActive: isPathActive) + isPathNavigable: isPathNavigable) } } } @@ -47,9 +44,6 @@ struct MusicDestinationModifier: ViewModifier { extension View { func musicDestinations(_ vault: Vault, path: [ArchivePath] = []) -> some View { - modifier( - MusicDestinationModifier( - vault: vault, isPathNavigable: path.isPathNavigable(_:), isPathActive: path.isPathActive(_:) - )) + modifier(MusicDestinationModifier(vault: vault, isPathNavigable: path.isPathNavigable(_:))) } } diff --git a/Sources/Site/Music/UI/PathRestorableUserActivityModifier.swift b/Sources/Site/Music/UI/PathRestorableUserActivityModifier.swift deleted file mode 100644 index e223f0fa..00000000 --- a/Sources/Site/Music/UI/PathRestorableUserActivityModifier.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// PathRestorableUserActivityModifier.swift -// -// -// Created by Greg Bolsinga on 6/21/23. -// - -import SwiftUI -import os - -extension ArchivePath { - static let activityType = "gdb.SiteApp.view-archivePath" -} - -extension Logger { - fileprivate static let pathActivity = Logger(category: "pathActivity") -} - -protocol PathRestorableUserActivity: PathRestorable { - var url: URL? { get } - func updateActivity(_ userActivity: NSUserActivity) -} - -struct PathRestorableUserActivityModifier: ViewModifier { - let item: T - let isPathActive: (T) -> Bool - - func body(content: Content) -> some View { - let isActive = isPathActive(item) - Logger.pathActivity.log( - "\(item.archivePath.formatted(.json), privacy: .public) active: \(isActive, privacy: .public)" - ) - return - content - .userActivity(ArchivePath.activityType, isActive: isActive) { userActivity in - userActivity.update(item) - } - } -} - -extension View { - func pathRestorableUserActivityModifier( - _ item: T, isPathActive: @escaping ((PathRestorable) -> Bool) - ) -> some View { - modifier(PathRestorableUserActivityModifier(item: item, isPathActive: isPathActive)) - } -} diff --git a/Sources/Site/Music/UI/ShowDetail.swift b/Sources/Site/Music/UI/ShowDetail.swift index 857c9fef..7a316c90 100644 --- a/Sources/Site/Music/UI/ShowDetail.swift +++ b/Sources/Site/Music/UI/ShowDetail.swift @@ -10,7 +10,6 @@ import SwiftUI struct ShowDetail: View { let concert: Concert let isPathNavigable: (PathRestorable) -> Bool - let isPathActive: (PathRestorable) -> Bool private var venueName: String { guard let venue = concert.venue else { @@ -61,7 +60,6 @@ struct ShowDetail: View { .listStyle(.grouped) #endif .navigationTitle(venueName) - .pathRestorableUserActivityModifier(concert, isPathActive: isPathActive) .archiveShare(concert, url: concert.url) } } @@ -72,9 +70,6 @@ struct ShowDetail: View { concert: vaultPreviewData.concerts[0], isPathNavigable: { _ in true - }, - isPathActive: { _ in - true } ) .musicDestinations(vaultPreviewData) @@ -87,9 +82,6 @@ struct ShowDetail: View { concert: vaultPreviewData.concerts[1], isPathNavigable: { _ in true - }, - isPathActive: { _ in - true } ) .musicDestinations(vaultPreviewData) @@ -102,9 +94,6 @@ struct ShowDetail: View { concert: vaultPreviewData.concerts[2], isPathNavigable: { _ in true - }, - isPathActive: { _ in - true } ) .musicDestinations(vaultPreviewData) diff --git a/Sources/Site/Music/UI/VenueDetail.swift b/Sources/Site/Music/UI/VenueDetail.swift index 2b56d7ff..d0c34263 100644 --- a/Sources/Site/Music/UI/VenueDetail.swift +++ b/Sources/Site/Music/UI/VenueDetail.swift @@ -16,7 +16,6 @@ struct VenueDetail: View { let concertCompare: (Concert, Concert) -> Bool let geocode: geocoder? let isPathNavigable: (PathRestorable) -> Bool - let isPathActive: (PathRestorable) -> Bool @State private var item: MKMapItem? = nil @@ -94,7 +93,6 @@ struct VenueDetail: View { .listStyle(.grouped) #endif .navigationTitle(digest.venue.name) - .pathRestorableUserActivityModifier(digest, isPathActive: isPathActive) .archiveShare(digest.venue, url: digest.url) } } @@ -107,9 +105,6 @@ struct VenueDetail: View { geocode: nil, isPathNavigable: { _ in true - }, - isPathActive: { _ in - true } ) .musicDestinations(vaultPreviewData) diff --git a/Sources/Site/Music/UI/YearDetail.swift b/Sources/Site/Music/UI/YearDetail.swift index 69761f95..eb33919d 100644 --- a/Sources/Site/Music/UI/YearDetail.swift +++ b/Sources/Site/Music/UI/YearDetail.swift @@ -11,7 +11,6 @@ struct YearDetail: View { let digest: AnnumDigest let concertCompare: (Concert, Concert) -> Bool let isPathNavigable: (PathRestorable) -> Bool - let isPathActive: (PathRestorable) -> Bool private var concerts: [Concert] { digest.concerts @@ -48,7 +47,6 @@ struct YearDetail: View { .listStyle(.grouped) #endif .navigationTitle(Text(digest.annum.formatted())) - .pathRestorableUserActivityModifier(digest, isPathActive: isPathActive) .archiveShare(digest.annum, url: digest.url) } } @@ -60,9 +58,6 @@ struct YearDetail: View { concertCompare: vaultPreviewData.comparator.compare(lhs:rhs:), isPathNavigable: { _ in true - }, - isPathActive: { _ in - true } ) .musicDestinations(vaultPreviewData) diff --git a/Sources/Site/Music/Vault+ArchivePath.swift b/Sources/Site/Music/Vault+ArchivePath.swift new file mode 100644 index 00000000..60275279 --- /dev/null +++ b/Sources/Site/Music/Vault+ArchivePath.swift @@ -0,0 +1,23 @@ +// +// Vault+ArchivePath.swift +// site +// +// Created by Greg Bolsinga on 9/30/24. +// + +import Foundation + +extension Vault { + func pathUserActivity(for path: ArchivePath) -> PathRestorableUserActivity? { + switch path { + case .show(let iD): + return concertMap[iD] + case .venue(let iD): + return venueDigestMap[iD] + case .artist(let iD): + return artistDigestMap[iD] + case .year(let annum): + return digest(for: annum) + } + } +} diff --git a/Tests/SiteTests/ArchiveNavigationTests.swift b/Tests/SiteTests/ArchiveNavigationTests.swift index 983a490c..294fedd9 100644 --- a/Tests/SiteTests/ArchiveNavigationTests.swift +++ b/Tests/SiteTests/ArchiveNavigationTests.swift @@ -171,4 +171,19 @@ struct ArchiveNavigationTests { #expect(!ar.userActivityActive(for: $0)) } } + + @Test("userActivity - Path", arguments: [[], [ArchivePath.artist("id")]]) + func userActivity(path: [ArchivePath]) { + let ar = ArchiveNavigation( + ArchiveNavigation.State( + category: .today, + categoryPaths: [.today: path])) + + #expect(!ar.userActivityActive(for: ArchivePath.venue("id"))) + if path.isEmpty { + #expect(!ar.userActivityActive(for: ArchivePath.artist("id"))) + } else { + #expect(ar.userActivityActive(for: ArchivePath.artist("id"))) + } + } }