Fix optional ID case for libraries (#1352)

This commit is contained in:
Ethan Pippin 2024-12-09 17:18:13 -07:00 committed by GitHub
parent 174487a220
commit bbfa944b52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 59 additions and 5 deletions

View File

@ -27,6 +27,13 @@ extension BaseItemDto: LibraryParent {
} }
} }
extension BaseItemDto: LibraryIdentifiable {
var unwrappedIDHashOrZero: Int {
id?.hashValue ?? 0
}
}
extension BaseItemDto { extension BaseItemDto {
var episodeLocator: String? { var episodeLocator: String? {

View File

@ -13,6 +13,10 @@ import UIKit
extension BaseItemPerson: Poster { extension BaseItemPerson: Poster {
var unwrappedIDHashOrZero: Int {
id?.hashValue ?? 0
}
var subtitle: String? { var subtitle: String? {
firstRole firstRole
} }

View File

@ -45,6 +45,10 @@ extension ChapterInfo {
chapterInfo.displayTitle chapterInfo.displayTitle
} }
var unwrappedIDHashOrZero: Int {
id
}
let systemImage: String = "film" let systemImage: String = "film"
var subtitle: String? var subtitle: String?
var showTitle: Bool = true var showTitle: Bool = true

View File

@ -42,6 +42,10 @@ struct ChannelProgram: Hashable, Identifiable {
extension ChannelProgram: Poster { extension ChannelProgram: Poster {
var unwrappedIDHashOrZero: Int {
channel.id?.hashValue ?? 0
}
var displayTitle: String { var displayTitle: String {
channel.displayTitle channel.displayTitle
} }

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
/// A type that is displayed as a poster /// A type that is displayed as a poster
protocol Poster: Displayable, Hashable, Identifiable, SystemImageable { protocol Poster: Displayable, Hashable, LibraryIdentifiable, SystemImageable {
/// Optional subtitle when used as a poster /// Optional subtitle when used as a poster
var subtitle: String? { get } var subtitle: String? { get }

View File

@ -18,6 +18,30 @@ import UIKit
/// Magic number for page sizes /// Magic number for page sizes
private let DefaultPageSize = 50 private let DefaultPageSize = 50
/// A protocol for items to conform to if they may be present within a library.
///
/// Similar to `Identifiable`, but `unwrappedIDHashOrZero` is an `Int`: the hash of the underlying `id`
/// value if it is not optional, or if it is optional it must return the hash of the wrapped value,
/// or 0 otherwise:
///
/// struct Item: LibraryIdentifiable {
/// var id: String? { "id" }
///
/// var unwrappedIDHashOrZero: Int {
/// // Gets the `hashValue` of the `String.hashValue`, not `Optional.hashValue`.
/// id?.hashValue ?? 0
/// }
/// }
///
/// This is necessary because if the `ID` is optional, then `Optional.hashValue` will be used instead
/// and result in differing hashes.
///
/// This also helps if items already conform to `Identifiable`, but has an optionally-typed `id`.
protocol LibraryIdentifiable: Identifiable {
var unwrappedIDHashOrZero: Int { get }
}
// TODO: fix how `hasNextPage` is determined // TODO: fix how `hasNextPage` is determined
// - some subclasses might not have "paging" and only have one call. This can be solved with // - some subclasses might not have "paging" and only have one call. This can be solved with
// a check if elements were actually appended to the set but that requires a redundant get // a check if elements were actually appended to the set but that requires a redundant get
@ -33,7 +57,7 @@ private let DefaultPageSize = 50
on remembering other filters. on remembering other filters.
*/ */
class PagingLibraryViewModel<Element: Poster & Identifiable>: ViewModel, Eventful, Stateful { class PagingLibraryViewModel<Element: Poster & LibraryIdentifiable>: ViewModel, Eventful, Stateful {
// MARK: Event // MARK: Event
@ -105,11 +129,21 @@ class PagingLibraryViewModel<Element: Poster & Identifiable>: ViewModel, Eventfu
parent: (any LibraryParent)? = nil parent: (any LibraryParent)? = nil
) { ) {
self.filterViewModel = nil self.filterViewModel = nil
self.elements = IdentifiedArray(uniqueElements: data, id: \.id.hashValue) self.elements = IdentifiedArray(uniqueElements: data, id: \.unwrappedIDHashOrZero)
self.isStatic = true self.isStatic = true
self.hasNextPage = false self.hasNextPage = false
self.pageSize = DefaultPageSize self.pageSize = DefaultPageSize
self.parent = parent self.parent = parent
super.init()
Notifications[.didDeleteItem]
.publisher
.receive(on: RunLoop.main)
.sink { id in
self.elements.remove(id: id.hashValue)
}
.store(in: &cancellables)
} }
convenience init( convenience init(
@ -132,7 +166,7 @@ class PagingLibraryViewModel<Element: Poster & Identifiable>: ViewModel, Eventfu
filters: ItemFilterCollection? = nil, filters: ItemFilterCollection? = nil,
pageSize: Int = DefaultPageSize pageSize: Int = DefaultPageSize
) { ) {
self.elements = IdentifiedArray(id: \.id.hashValue) self.elements = IdentifiedArray(id: \.unwrappedIDHashOrZero)
self.isStatic = false self.isStatic = false
self.pageSize = pageSize self.pageSize = pageSize
self.parent = parent self.parent = parent

View File

@ -36,7 +36,7 @@ import SwiftUI
should be applied. should be applied.
*/ */
struct PagingLibraryView<Element: Poster & Identifiable>: View { struct PagingLibraryView<Element: Poster>: View {
@Default(.Customization.Library.enabledDrawerFilters) @Default(.Customization.Library.enabledDrawerFilters)
private var enabledDrawerFilters private var enabledDrawerFilters
@ -240,6 +240,7 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
private var gridView: some View { private var gridView: some View {
CollectionVGrid( CollectionVGrid(
uniqueElements: viewModel.elements, uniqueElements: viewModel.elements,
id: \.unwrappedIDHashOrZero,
layout: layout layout: layout
) { item in ) { item in