Skip to content

Commit

Permalink
Incorporate now playing into filtering to prevent UX confusion
Browse files Browse the repository at this point in the history
  • Loading branch information
allenhumphreys committed Jun 29, 2023
1 parent 2ad7f49 commit b9a5585
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 111 deletions.
7 changes: 5 additions & 2 deletions WWDC/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,14 @@ final class AppCoordinator: Logging {
exploreItem.label = "Explore"
tabController.addTabViewItem(exploreItem)

_playerOwnerSessionIdentifier = .init(initialValue: nil)
// Schedule
scheduleController = ScheduleContainerViewController(
windowController: windowController,
rowProvider: ScheduleSessionRowProvider(
scheduleSections: storage.scheduleShallowObservable,
filterPredicate: searchCoordinator.$scheduleFilterPredicate
filterPredicate: searchCoordinator.$scheduleFilterPredicate,
playingSessionIdentifier: _playerOwnerSessionIdentifier.projectedValue
),
searchController: scheduleSearchController
)
Expand All @@ -102,7 +104,8 @@ final class AppCoordinator: Logging {
windowController: windowController,
rowProvider: VideosSessionRowProvider(
tracks: storage.tracks,
filterPredicate: searchCoordinator.$videosFilterPredicate
filterPredicate: searchCoordinator.$videosFilterPredicate,
playingSessionIdentifier: _playerOwnerSessionIdentifier.projectedValue
),
searchController: videosSearchController
)
Expand Down
58 changes: 39 additions & 19 deletions WWDC/SessionRowProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ final class VideosSessionRowProvider: SessionRowProvider, Logging {
@Published var rows: SessionRows?
var rowsPublisher: AnyPublisher<SessionRows, Never> { $rows.dropFirst().compacted().eraseToAnyPublisher() }

init<P: Publisher>(tracks: Results<Track>, filterPredicate: P) where P.Output == FilterPredicate, P.Failure == Never {
init<P: Publisher, PlayingSession: Publisher>(
tracks: Results<Track>,
filterPredicate: P,
playingSessionIdentifier: PlayingSession
) where P.Output == FilterPredicate, P.Failure == Never, PlayingSession.Output == String?, PlayingSession.Failure == Never {
let tracksAndSessions = tracks.collectionChangedPublisher
.replaceErrorWithEmpty()
.map {
Expand Down Expand Up @@ -170,18 +174,27 @@ final class VideosSessionRowProvider: SessionRowProvider, Logging {
.do {
Self.log.debug("Filter predicate updated")
}
Publishers.CombineLatest(
Publishers.CombineLatest3(
tracksAndSessions.replaceErrorWithEmpty(),
filterPredicate
filterPredicate,
playingSessionIdentifier
)
.map { (allViewModels, predicate) in
.map { (allViewModels, predicate, playingSessionIdentifier) in
let effectivePredicate: NSPredicate
if let playingSessionIdentifier {
effectivePredicate = NSCompoundPredicate(
orPredicateWithSubpredicates: [predicate.predicate ?? NSPredicate(value: true), NSPredicate(format: "identifier == %@", playingSessionIdentifier)]
)
} else {
effectivePredicate = predicate.predicate ?? NSPredicate(value: true)
}
// Observe the filtered sessions
// These observers emit elements in the case of a session having some property changed that is
// affected by the query. e.g. Filtering on favorites then unfavorite a session
allViewModels.map { (key: SessionRow, value: (Results<Session>, OrderedDictionary<String, SessionRow>)) in
return allViewModels.map { (key: SessionRow, value: (Results<Session>, OrderedDictionary<String, SessionRow>)) in
let (allTrackSessions, sessionRows) = value
return allTrackSessions
.filter(predicate.predicate ?? NSPredicate(value: true))
.filter(effectivePredicate)
.collectionChangedPublisher
.replaceErrorWithEmpty()
.map {
Expand All @@ -194,11 +207,8 @@ final class VideosSessionRowProvider: SessionRowProvider, Logging {
}
}
.switchToLatest()
.map { thing in
Self.log.debug("Received new update")

return Self.sessionRows(thing)
}
.map(Self.sessionRows)
.do { Self.log.debug("Received new update") }
.assign(to: &$rows)
}

Expand Down Expand Up @@ -253,10 +263,11 @@ final class ScheduleSessionRowProvider: SessionRowProvider, Logging {
@Published var rows: SessionRows?
var rowsPublisher: AnyPublisher<SessionRows, Never> { $rows.dropFirst().compacted().eraseToAnyPublisher() }

init<P: Publisher, S: Publisher>(
init<P: Publisher, S: Publisher, PlayingSession: Publisher>(
scheduleSections: S,
filterPredicate: P
) where P.Output == FilterPredicate, P.Failure == Never, S.Output == Results<ScheduleSection> {
filterPredicate: P,
playingSessionIdentifier: PlayingSession
) where P.Output == FilterPredicate, P.Failure == Never, S.Output == Results<ScheduleSection>, PlayingSession.Output == String?, PlayingSession.Failure == Never {

let sectionsAndSessions = scheduleSections
.replaceErrorWithEmpty()
Expand Down Expand Up @@ -289,18 +300,27 @@ final class ScheduleSessionRowProvider: SessionRowProvider, Logging {
})
.do { Self.log.debug("Filter predicate updated") }

Publishers.CombineLatest(
Publishers.CombineLatest3(
sectionsAndSessions.replaceErrorWithEmpty(),
filterPredicate
filterPredicate,
playingSessionIdentifier
)
.map { (allViewModels, predicate) in
.map { (allViewModels, predicate, playingSessionIdentifier) in
let effectivePredicate: NSPredicate
if let playingSessionIdentifier {
effectivePredicate = NSCompoundPredicate(
orPredicateWithSubpredicates: [predicate.predicate ?? NSPredicate(value: true), NSPredicate(format: "identifier == %@", playingSessionIdentifier)]
)
} else {
effectivePredicate = predicate.predicate ?? NSPredicate(value: true)
}
// Observe the filtered sessions
// These observers emit elements in the case of a session having some property changed that is
// affected by the query. e.g. Filtering on favorites then unfavorite a session
allViewModels.map { (key: SessionRow, value: (Results<SessionInstance>, OrderedDictionary<String, SessionRow>)) in
return allViewModels.map { (key: SessionRow, value: (Results<SessionInstance>, OrderedDictionary<String, SessionRow>)) in
let (allSectionInstances, sessionRows) = value
return allSectionInstances
.filter(predicate.predicate ?? NSPredicate(value: true))
.filter(effectivePredicate)
.collectionChangedPublisher
.replaceErrorWithEmpty()
.map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,93 +82,3 @@ extension Array where Element == SessionRow {
}
}
}

final class FilterResults: Logging {
static let log = makeLogger()

static var empty: FilterResults {
return FilterResults(storage: nil, query: nil)
}

private let query: NSPredicate?

private let storage: Storage?

private(set) var latestSearchResults: Results<Session>?

private lazy var cancellables: Set<AnyCancellable> = []
private var nowPlayingBag: Set<AnyCancellable> = []

private var observerClosure: ((Results<Session>?) -> Void)?
private var observerToken: NotificationToken?

init(storage: Storage?, query: NSPredicate?) {
self.storage = storage
self.query = query

if let coordinator = (NSApplication.shared.delegate as? AppDelegate)?.coordinator {

coordinator
.$playerOwnerSessionIdentifier
.sink(receiveValue: { [weak self] _ in
self?.bindResults()
})
.store(in: &nowPlayingBag)
}
}

func observe(with closure: @escaping (Results<Session>?) -> Void) {
assert(observerClosure == nil)

guard query != nil, storage != nil else {
closure(nil)
return
}

observerClosure = closure

bindResults()
}

private func bindResults() {
guard let observerClosure = observerClosure else { return }
guard let storage = storage, let query = query?.orCurrentlyPlayingSession() else { return }

cancellables = []

do {
let realm = try Realm(configuration: storage.realmConfig)

let objects = realm.objects(Session.self).filter(query)

// Immediately provide the first value
self.latestSearchResults = objects
observerClosure(objects)

objects
.collectionChangedPublisher
.dropFirst(1) // first value is provided synchronously to help with timing issues
.replaceErrorWithEmpty()
.sink { [weak self] in
self?.latestSearchResults = $0
observerClosure($0)
}
.store(in: &cancellables)
} catch {
observerClosure(nil)
log.error("Failed to initialize Realm for searching: \(String(describing: error), privacy: .public)")
}
}
}

fileprivate extension NSPredicate {

func orCurrentlyPlayingSession() -> NSPredicate {

guard let playingSession = (NSApplication.shared.delegate as? AppDelegate)?.coordinator?.playerOwnerSessionIdentifier else {
return self
}

return NSCompoundPredicate(orPredicateWithSubpredicates: [self, NSPredicate(format: "identifier == %@", playingSession)])
}
}

0 comments on commit b9a5585

Please sign in to comment.