Skip to content

Commit

Permalink
Add ArchiveActivity (#943)
Browse files Browse the repository at this point in the history
This is the current UI @State. It simplifies this so that more may use
it. Nice clean up.
  • Loading branch information
bolsinga authored Oct 4, 2024
1 parent a42affb commit 2ccecbd
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 93 deletions.
4 changes: 0 additions & 4 deletions Sources/Site/Music/ArchivePath+PathRestorable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,4 @@ extension Array where Element == ArchivePath {
// Drop the last path so that when going back the state is correct. Otherwise the '>' will flash on after animating in.
return !self.dropLast().contains { $0 == archivePath }
}

func isPathActive(_ archivePath: ArchivePath) -> Bool {
self.last == archivePath
}
}
46 changes: 46 additions & 0 deletions Sources/Site/Music/UI/ArchiveActivity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// ArchiveActivity.swift
// site
//
// Created by Greg Bolsinga on 10/4/24.
//

import Foundation

enum ArchiveActivity: Equatable {
/// Nothing is active. Only possible on iOS or tvOS
case none
/// Just a category is selected.
case category(ArchiveCategory)
/// This path is the top of the navigation stack.
case path(ArchivePath)
}

extension ArchiveActivity {
var isCategory: Bool {
if case .category(_) = self {
return true
}
return false
}

var isPath: Bool {
if case .path(_) = self {
return true
}
return false
}
}

extension ArchiveActivity: CustomStringConvertible {
var description: String {
switch self {
case .none:
"none"
case .category(let archiveCategory):
"category: \(archiveCategory.rawValue)"
case .path(let archivePath):
"path: \(archivePath.formatted(.json))"
}
}
}
21 changes: 6 additions & 15 deletions Sources/Site/Music/UI/ArchiveNavigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,12 @@ extension Logger {
state = State(category: category)
}

func userActivityActive(for category: ArchiveCategory) -> Bool {
guard category == self.category else {
// category is not the same as selected category.
return false
}
guard let path = state.categoryPaths[category] else {
// category same, but path is nil
return true
}
// category is active if its path is empty
return path.isEmpty
}

func userActivityActive(for path: ArchivePath) -> Bool {
self.path.isPathActive(path)
var activity: ArchiveActivity {
#if os(iOS) || os(tvOS)
guard let category = state.category else { return .none }
#endif
guard !path.isEmpty, let last = path.last else { return .category(category) }
return .path(last)
}
}

Expand Down
73 changes: 24 additions & 49 deletions Sources/Site/Music/UI/ArchiveNavigationUserActivityModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,61 +29,36 @@ struct ArchiveNavigationUserActivityModifier: ViewModifier {
let urlForCategory: (ArchiveCategory.DefaultCategory) -> URL?
let activityForPath: (ArchivePath) -> PathRestorableUserActivity?

@State private var userActivityCategory: ArchiveCategory.DefaultCategory = .defaultCategory
@State private var userActivityPath: [ArchivePath] = []

private var isCategoryActive: Bool {
#if os(iOS) || os(tvOS)
guard let userActivityCategory else { return false }
#endif
return archiveNavigation.userActivityActive(for: userActivityCategory)
}

private var isPathActive: Bool {
guard let lastPath = userActivityPath.last else { return false }
return archiveNavigation.userActivityActive(for: lastPath)
}
@State private var activity = ArchiveActivity.none

func body(content: Content) -> some View {
let isCategoryActive = isCategoryActive
let isPathActive = !isCategoryActive && isPathActive

#if os(iOS) || os(tvOS)
Logger.navigationUserActivity.log(
"\(userActivityCategory?.rawValue ?? "nil", privacy: .public): \(isCategoryActive, privacy: .public) \(userActivityPath, privacy: .public): \(isPathActive)"
)
#elseif os(macOS)
Logger.navigationUserActivity.log(
"\(userActivityCategory.rawValue, privacy: .public): \(isCategoryActive, privacy: .public) \(userActivityPath, privacy: .public): \(isPathActive)"
)
#endif

Logger.navigationUserActivity.log("activity: \(activity, privacy: .public)")
return
content
.onAppear {
userActivityCategory = archiveNavigation.category
userActivityPath = archiveNavigation.path
}
.onChange(of: archiveNavigation.category) { _, newValue in
userActivityCategory = newValue
}
.onChange(of: archiveNavigation.path) { _, newValue in
userActivityPath = newValue
.onAppear { activity = archiveNavigation.activity }
.onChange(of: archiveNavigation.activity) { _, newValue in
activity = 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(ArchiveCategory.activityType, isActive: activity.isCategory) { userActivity in
switch activity {
case .none, .path(_):
return
case .category(let archiveCategory):
Logger.navigationUserActivity.log(
"update category \(archiveCategory.rawValue, privacy: .public)")
userActivity.update(archiveCategory, url: urlForCategory(archiveCategory))
}
}
.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)
.userActivity(ArchivePath.activityType, isActive: activity.isPath) { userActivity in
switch activity {
case .none, .category(_):
return
case .path(let archivePath):
Logger.navigationUserActivity.log(
"update path \(archivePath.formatted(.json), privacy: .public)")
guard let pathUserActivity = activityForPath(archivePath) else { return }
userActivity.update(pathUserActivity)
}
}
}
}
Expand Down
80 changes: 55 additions & 25 deletions Tests/SiteTests/ArchiveNavigationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ import Testing

@testable import Site

extension ArchiveActivity {
var isNone: Bool {
if case .none = self {
return true
}
return false
}

func matches(category: ArchiveCategory) -> Bool {
if case let .category(cat) = self {
return cat == category
}
return false
}

func matches(path: ArchivePath) -> Bool {
if case let .path(ap) = self {
return ap == path
}
return false
}
}

struct ArchiveNavigationTests {
@Test func navigateToCategory() {
let ar = ArchiveNavigation()
Expand Down Expand Up @@ -142,43 +165,50 @@ struct ArchiveNavigationTests {
#expect(ar.category == .defaultCategory)
}

@Test
func userActivity_default_noPath() throws {
#if os(iOS) || os(tvOS)
@Test("Activity - nil")
func activityNone() throws {
let ar = ArchiveNavigation(ArchiveNavigation.State(category: nil))
let activity = ar.activity

#expect(activity.isNone)
#expect(!activity.isCategory)
#expect(!activity.isPath)
}
#endif

@Test("Activity - DefaultCategory")
func activityDefault() throws {
let ar = ArchiveNavigation()
let activity = ar.activity

#expect(!activity.isNone)
#expect(activity.isCategory)
#expect(!activity.isPath)

#if os(iOS) || os(tvOS)
try #require(ArchiveCategory.defaultCategory != nil)
#expect(ar.userActivityActive(for: .defaultCategory!))
#expect(activity.matches(category: .defaultCategory!))
#else
#expect(ar.userActivityActive(for: .defaultCategory))
#expect(activity.matches(category: .defaultCategory))
#endif
}

@Test(
"userActivity - Category", arguments: ArchiveCategory.allCases,
[[], [ArchivePath.artist("id")]])
func userActivity(category: ArchiveCategory, path: [ArchivePath]) {
@Test("Activity", arguments: ArchiveCategory.allCases, [[], [ArchivePath.artist("id")]])
func activity(category: ArchiveCategory, path: [ArchivePath]) {
let ar = ArchiveNavigation(
ArchiveNavigation.State(category: category, categoryPaths: [category: path]))
let activity = ar.activity

#expect(path.isEmpty || !ar.userActivityActive(for: category))
#expect(!path.isEmpty || ar.userActivityActive(for: category))

ArchiveCategory.allCases.filter { $0 != category }.forEach {
#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(!activity.isNone)
#expect(activity.isCategory || !path.isEmpty)
#expect(activity.isPath || path.isEmpty)

#expect(!ar.userActivityActive(for: ArchivePath.venue("id")))
#expect(activity.matches(category: category) || !path.isEmpty)
#expect(activity.matches(path: ArchivePath.artist("id")) || path.isEmpty)

#expect(path.isEmpty || ar.userActivityActive(for: ArchivePath.artist("id")))
#expect(!path.isEmpty || !ar.userActivityActive(for: ArchivePath.artist("id")))
ArchiveCategory.allCases.filter { $0 != category }.forEach { categoryCase in
#expect(!activity.matches(category: categoryCase))
}
}
}

0 comments on commit 2ccecbd

Please sign in to comment.