From 2f416ea71b7c0dd16a3b5c8f3b79ef681d0bc948 Mon Sep 17 00:00:00 2001 From: Joseph Mattiello Date: Fri, 22 Nov 2024 06:11:19 -0500 Subject: [PATCH] Fix PVGame viewmodel realm crash, use @Persisted Signed-off-by: Joseph Mattiello --- .../GameImporterDatabaseService.swift | 4 +- .../Entities/Files/PVImageFile.swift | 10 +- .../RealmPlatform/Entities/PVBIOS.swift | 20 ++-- .../RealmPlatform/Entities/PVCore.swift | 22 ++-- .../RealmPlatform/Entities/PVGame.swift | 103 ++++++++---------- .../RealmPlatform/Entities/PVLibrary.swift | 18 +-- .../Entities/PVLibraryEntry.swift | 2 +- .../RealmPlatform/Entities/PVRecentGame.swift | 12 +- .../RealmPlatform/Entities/PVSaveState.swift | 22 ++-- .../RealmPlatform/Entities/PVSystem.swift | 48 ++++---- .../RealmPlatform/Entities/PVUser.swift | 10 +- .../ConsoleGamesView+GamepadNavigation.swift | 37 +++---- .../PVSwiftUI/Consoles/ConsoleGamesView.swift | 78 ++++++++++--- .../Consoles/ConsoleGamesViewModel.swift | 51 --------- .../PVSwiftUI/GameItem/GameItemView.swift | 1 - 15 files changed, 205 insertions(+), 233 deletions(-) diff --git a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterDatabaseService.swift b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterDatabaseService.swift index 862a3b7edd..c904ca5e05 100644 --- a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterDatabaseService.swift +++ b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterDatabaseService.swift @@ -283,8 +283,8 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing { game.regionName = regionName } - if let regionID = gameDBRecordInfo["regionID"] as? Int, forceRefresh || game.regionID.value == nil { - game.regionID.value = regionID + if let regionID = gameDBRecordInfo["regionID"] as? Int, forceRefresh || game.regionID == nil { + game.regionID = regionID } if let gameDescription = gameDBRecordInfo["gameDescription"] as? String, !gameDescription.isEmpty, forceRefresh || game.gameDescription == nil { diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVImageFile.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVImageFile.swift index 04b253e77e..158f5294d2 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVImageFile.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVImageFile.swift @@ -21,11 +21,11 @@ import PVFileSystem @objcMembers public final class PVImageFile: PVFile { - public internal(set) dynamic var _cgsize: String! - public dynamic var ratio: Float = 0.0 - public dynamic var width: Int = 0 - public dynamic var height: Int = 0 - public dynamic var layout: String = "" + @Persisted public internal(set) dynamic var _cgsize: String! + @Persisted public var ratio: Float = 0.0 + @Persisted public var width: Int = 0 + @Persisted public var height: Int = 0 + @Persisted public var layout: String = "" public convenience init(withPartialPath partialPath: String, relativeRoot: RelativeRoot = RelativeRoot.platformDefault) { self.init() diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVBIOS.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVBIOS.swift index e2cb07d09d..ffc2d962bf 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVBIOS.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVBIOS.swift @@ -15,23 +15,19 @@ public final class PVBIOS: Object, Identifiable, BIOSFileProvider { // public var status: BIOSStatus public var id: String { expectedMD5 } - public dynamic var system: PVSystem! + @Persisted public var system: PVSystem! - public dynamic var descriptionText: String = "" + @Persisted public var descriptionText: String = "" public var regions: RegionOptions = .unknown - public dynamic var version: String = "" - public dynamic var optional: Bool = false + @Persisted public var version: String = "" + @Persisted public var optional: Bool = false - public dynamic var expectedMD5: String = "" - public dynamic var expectedSize: Int = 0 - public dynamic var expectedFilename: String = "" + @Persisted(indexed: true) public var expectedMD5: String = "" + @Persisted public var expectedSize: Int = 0 + @Persisted(primaryKey: true) public var expectedFilename: String = "" - public dynamic var file: PVFile? + @Persisted public var file: PVFile? public var fileInfo: PVFile? { return file } - - public override static func primaryKey() -> String? { - return "expectedFilename" - } } public extension PVBIOS { diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVCore.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVCore.swift index 930f2778f9..0961463b89 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVCore.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVCore.swift @@ -13,15 +13,15 @@ import PVPrimitives @objcMembers public final class PVCore: RealmSwift.Object, Identifiable { - public dynamic var identifier: String = "" - public dynamic var principleClass: String = "" - public var supportedSystems = List() + @Persisted(primaryKey: true) public var identifier: String = "" + @Persisted public var principleClass: String = "" + @Persisted public var supportedSystems: List - public dynamic var projectName = "" - public dynamic var projectURL = "" - public dynamic var projectVersion = "" - public dynamic var disabled = false - public dynamic var appStoreDisabled = false + @Persisted public var projectName = "" + @Persisted public var projectURL = "" + @Persisted public var projectVersion = "" + @Persisted public var disabled = false + @Persisted public var appStoreDisabled = false public var hasCoreClass: Bool { let _class: AnyClass? = NSClassFromString(principleClass) @@ -30,7 +30,7 @@ public final class PVCore: RealmSwift.Object, Identifiable { } // Reverse links - public var saveStates = LinkingObjects(fromType: PVSaveState.self, property: "core") + @Persisted(originProperty: "core") public var saveStates: LinkingObjects public convenience init(withIdentifier identifier: String, principleClass: String, supportedSystems: [PVSystem], name: String, url: String, version: String, disabled: Bool = false, appStoreDisabled: Bool = false) { self.init() @@ -45,10 +45,6 @@ public final class PVCore: RealmSwift.Object, Identifiable { self.appStoreDisabled = appStoreDisabled } - public override static func primaryKey() -> String? { - return "identifier" - } - public override class func ignoredProperties() -> [String] { ["hasCoreClass", "id"] } diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVGame.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVGame.swift index 91b7f9f000..08026cd6e9 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVGame.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVGame.swift @@ -14,8 +14,8 @@ import PVFileSystem @objcMembers public final class PVGame: RealmSwift.Object, Identifiable, PVGameLibraryEntry { - public dynamic var title: String = "" - public dynamic var id :String = NSUUID().uuidString + @Persisted public var title: String = "" + @Persisted(wrappedValue: NSUUID().uuidString) public var id :String // TODO: This is a 'partial path' meaing it's something like {system id}.filename // We should make this an absolute path but would need a Realm translater and modifying @@ -23,44 +23,51 @@ public final class PVGame: RealmSwift.Object, Identifiable, PVGameLibraryEntry { // and then we just need to change that method but I haven't check that every method uses that // The other option is to only use the filename and then path(forGame:) would determine the // fully qualified path, but if we add network / cloud storage that may or may not change that. - public dynamic var romPath: String = "" - public dynamic var file: PVFile! - public private(set) var relatedFiles = List() + @Persisted public var romPath: String = "" + @Persisted public var file: PVFile! + @Persisted public private(set) var relatedFiles = List() - public dynamic var customArtworkURL: String = "" - public dynamic var originalArtworkURL: String = "" - public dynamic var originalArtworkFile: PVImageFile? + @Persisted public var customArtworkURL: String = "" + @Persisted public var originalArtworkURL: String = "" + @Persisted public var originalArtworkFile: PVImageFile? - public dynamic var requiresSync: Bool = true - public dynamic var isFavorite: Bool = false + @Persisted public var requiresSync: Bool = true + @Persisted(indexed: true) public var isFavorite: Bool = false - public dynamic var romSerial: String? - public dynamic var romHeader: String? - public private(set) dynamic var importDate: Date = Date() + @Persisted public var romSerial: String? + @Persisted public var romHeader: String? + @Persisted public private(set) var importDate: Date = Date() - public dynamic var systemIdentifier: String = "" - public dynamic var system: PVSystem! + @Persisted(indexed: true) public var systemIdentifier: String = "" + @Persisted public var system: PVSystem! - public dynamic var md5Hash: String = "" - public dynamic var crc: String = "" + /* + Primary key must be set at import time and can't be changed after. + I had to change the import flow to calculate the MD5 before import. + Seems sane enough since it's on the serial queue. Could always use + an async dispatch if it's an issue. - jm + */ + + @Persisted(primaryKey: true) public var md5Hash: String = "" + @Persisted public var crc: String = "" // If the user has set 'always use' for a specfic core // We don't use PVCore incase cores are removed / deleted - public dynamic var userPreferredCoreID: String? + @Persisted public var userPreferredCoreID: String? /* Links to other objects */ - public private(set) var saveStates = LinkingObjects(fromType: PVSaveState.self, property: "game") - public private(set) var cheats = LinkingObjects(fromType: PVCheats.self, property: "game") - public private(set) var recentPlays = LinkingObjects(fromType: PVRecentGame.self, property: "game") - public private(set) var screenShots = List() + @Persisted(originProperty: "game") public private(set) var saveStates: LinkingObjects + @Persisted(originProperty: "game") public private(set) var cheats: LinkingObjects + @Persisted(originProperty: "game") public private(set) var recentPlays: LinkingObjects + @Persisted public private(set) var screenShots = List() - public private(set) var libraries = LinkingObjects(fromType: PVLibrary.self, property: "games") + @Persisted(originProperty: "games") public private(set) var libraries: LinkingObjects /* Tracking data */ - public dynamic var lastPlayed: Date? - public dynamic var playCount: Int = 0 - public dynamic var timeSpentInGame: Int = 0 - public dynamic var rating: Int = -1 { + @Persisted public var lastPlayed: Date? + @Persisted public var playCount: Int = 0 + @Persisted public var timeSpentInGame: Int = 0 + @Persisted public var rating: Int = -1 { willSet { assert(-1 ... 5 ~= newValue, "Setting rating out of range -1 to 5") } @@ -72,35 +79,21 @@ public final class PVGame: RealmSwift.Object, Identifiable, PVGameLibraryEntry { } /* Extra metadata from OpenBG */ - public dynamic var gameDescription: String? - public dynamic var boxBackArtworkURL: String? - public dynamic var developer: String? - public dynamic var publisher: String? - public dynamic var publishDate: String? - public dynamic var genres: String? // Is a comma seperated list or single entry - public dynamic var referenceURL: String? - public dynamic var releaseID: String? - public dynamic var regionName: String? - public var regionID = RealmProperty() - public dynamic var systemShortName: String? - public dynamic var language: String? + @Persisted public var gameDescription: String? + @Persisted public var boxBackArtworkURL: String? + @Persisted public var developer: String? + @Persisted public var publisher: String? + @Persisted public var publishDate: String? + @Persisted public var genres: String? // Is a comma seperated list or single entry + @Persisted public var referenceURL: String? + @Persisted public var releaseID: String? + @Persisted public var regionName: String? + @Persisted public var regionID: Int? + @Persisted public var systemShortName: String? + @Persisted public var language: String? public var validatedGame: PVGame? { return self.isInvalidated ? nil : self } - /* - Primary key must be set at import time and can't be changed after. - I had to change the import flow to calculate the MD5 before import. - Seems sane enough since it's on the serial queue. Could always use - an async dispatch if it's an issue. - jm - */ - public override static func primaryKey() -> String? { - return "md5Hash" - } - - public override static func indexedProperties() -> [String] { - return ["systemIdentifier"] - } - public static func mockGenerate(systemID: String? = nil, count: Int = 10) -> [PVGame] { let systemIdentifier = systemID ?? "mock.system" return (1...count).map { index in @@ -186,7 +179,7 @@ public extension Game { let genres = game.genres let referenceURL = game.referenceURL let releaseID = game.releaseID - let regionID = game.regionID.value + let regionID = game.regionID let regionName = game.regionName let systemShortName = game.systemShortName let language = game.language @@ -265,7 +258,7 @@ extension Game: RealmRepresentable { object.referenceURL = referenceURL object.releaseID = releaseID object.regionName = regionName - object.regionID.value = regionID + object.regionID = regionID object.systemShortName = systemShortName object.language = language object.file = PVFile(withPartialPath: file.fileName) diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVLibrary.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVLibrary.swift index f7a0e444f3..d111eda2fb 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVLibrary.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVLibrary.swift @@ -16,20 +16,20 @@ import RealmSwift @objcMembers public final class PVLibrary: Object { - public dynamic var uuid: String = "" - public dynamic var name: String = "" + @Persisted public var uuid: String = "" + @Persisted public var name: String = "" - public dynamic var isLocal: Bool = true + @Persisted public var isLocal: Bool = true // Remote info - public dynamic var ipaddress: String = "" - public dynamic var domainname: String = "" - public dynamic var bonjourName: String = "" - public dynamic var port: Int = 7769 // prov on phone pad + @Persisted public var ipaddress: String = "" + @Persisted public var domainname: String = "" + @Persisted public var bonjourName: String = "" + @Persisted public var port: Int = 7769 // prov on phone pad - public dynamic var lastSeen: Date = Date() + @Persisted public var lastSeen: Date = Date() - public private(set) var games = List() + @Persisted public private(set) var games: List } // PVLibrary - Network diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVLibraryEntry.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVLibraryEntry.swift index 267e701d91..42a5efcb09 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVLibraryEntry.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVLibraryEntry.swift @@ -75,7 +75,7 @@ public protocol PVGameLibraryEntry: PVLibraryEntry { dynamic var referenceURL: String? { get } dynamic var releaseID: String? { get } dynamic var regionName: String? { get } - var regionID: RealmProperty { get } + var regionID: Int? { get } dynamic var systemShortName: String? { get } dynamic var language: String? { get } diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVRecentGame.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVRecentGame.swift index 3ec36558c9..133b2ce587 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVRecentGame.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVRecentGame.swift @@ -11,14 +11,10 @@ import RealmSwift @objcMembers public final class PVRecentGame: Object, Identifiable, PVRecentGameLibraryEntry { - public dynamic var id :String = NSUUID().uuidString - public dynamic var game: PVGame! - public dynamic var lastPlayedDate: Date = Date() - public dynamic var core: PVCore? - - public override static func indexedProperties() -> [String] { - return ["lastPlayedDate"] - } + @Persisted(wrappedValue: UUID().uuidString) public var id: String + @Persisted public var game: PVGame! + @Persisted(wrappedValue: Date(), indexed: true) public var lastPlayedDate: Date + @Persisted public var core: PVCore? public convenience init(withGame game: PVGame, core: PVCore? = nil) { self.init() diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVSaveState.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVSaveState.swift index 8d6d75cbba..7b7a395228 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVSaveState.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVSaveState.swift @@ -14,16 +14,16 @@ import PVPrimitives @objcMembers public final class PVSaveState: RealmSwift.Object, Identifiable, Filed, LocalFileProvider { - public dynamic var id = UUID().uuidString - public dynamic var game: PVGame! - public dynamic var core: PVCore! - public dynamic var file: PVFile! - public dynamic var date: Date = Date() - public dynamic var lastOpened: Date? - public dynamic var image: PVImageFile? - public dynamic var isAutosave: Bool = false + @Persisted(wrappedValue: UUID().uuidString, primaryKey: true) public var id: String + @Persisted public var game: PVGame! + @Persisted public var core: PVCore! + @Persisted public var file: PVFile! + @Persisted public var date: Date = Date() + @Persisted public var lastOpened: Date? + @Persisted public var image: PVImageFile? + @Persisted public var isAutosave: Bool = false - public dynamic var createdWithCoreVersion: String! + @Persisted public var createdWithCoreVersion: String! public convenience init(withGame game: PVGame, core: PVCore, file: PVFile, image: PVImageFile? = nil, isAutosave: Bool = false) { self.init() @@ -38,8 +38,4 @@ public final class PVSaveState: RealmSwift.Object, Identifiable, Filed, LocalFil public static func == (lhs: PVSaveState, rhs: PVSaveState) -> Bool { return lhs.file.url == rhs.file.url } - - public override static func primaryKey() -> String? { - return "id" - } } diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVSystem.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVSystem.swift index 321084d1ea..6c67ddc473 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVSystem.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVSystem.swift @@ -34,27 +34,27 @@ public final class PVSystem: Object, Identifiable, SystemProtocol { public typealias BIOSInfoProviderType = PVBIOS - public dynamic var name: String = "" - public dynamic var shortName: String = "" - public dynamic var shortNameAlt: String? - public dynamic var manufacturer: String = "" - public dynamic var releaseYear: Int = 0 - public dynamic var bit: Int = 0 + @Persisted(indexed: true) public var name: String = "" + @Persisted public var shortName: String = "" + @Persisted public var shortNameAlt: String? + @Persisted public var manufacturer: String = "" + @Persisted public var releaseYear: Int = 0 + @Persisted public var bit: Int = 0 public var bits: SystemBits { return SystemBits(rawValue: bit) ?? .unknown } - public dynamic var headerByteSize: Int = 0 - public dynamic var openvgDatabaseID: Int = 0 - public dynamic var requiresBIOS: Bool = false - public dynamic var usesCDs: Bool = false - public dynamic var portableSystem: Bool = false + @Persisted public var headerByteSize: Int = 0 + @Persisted public var openvgDatabaseID: Int = 0 + @Persisted public var requiresBIOS: Bool = false + @Persisted public var usesCDs: Bool = false + @Persisted public var portableSystem: Bool = false - public dynamic var supportsRumble: Bool = false - public dynamic var supported: Bool = true - public dynamic var appStoreDisabled: Bool = false + @Persisted public var supportsRumble: Bool = false + @Persisted public var supported: Bool = true + @Persisted public var appStoreDisabled: Bool = false - public dynamic var _screenType: String = ScreenType.unknown.rawValue + @Persisted public var _screenType: String = ScreenType.unknown.rawValue public var options: SystemOptions { var systemOptions = [SystemOptions]() @@ -65,7 +65,7 @@ public final class PVSystem: Object, Identifiable, SystemProtocol { return SystemOptions(systemOptions) } - public private(set) var supportedExtensions = List() + @Persisted public private(set) var supportedExtensions: List public var BIOSes: [PVBIOS]? { return Array(bioses) @@ -76,9 +76,9 @@ public final class PVSystem: Object, Identifiable, SystemProtocol { } // Reverse Links - public private(set) var bioses = LinkingObjects(fromType: PVBIOS.self, property: "system") - public private(set) var games = LinkingObjects(fromType: PVGame.self, property: "system") - public private(set) var cores = LinkingObjects(fromType: PVCore.self, property: "supportedSystems") + @Persisted(originProperty: "system") public private(set) var bioses: LinkingObjects + @Persisted(originProperty: "system") public private(set) var games: LinkingObjects + @Persisted(originProperty: "supportedSystems") public private(set) var cores: LinkingObjects public lazy var gameStructs: () -> [Game] = { [self] in games.map( { Game(withGame: $0) } ) @@ -98,18 +98,14 @@ public final class PVSystem: Object, Identifiable, SystemProtocol { return Core(with: preferredCore) } - public dynamic var userPreferredCoreID: String? + @Persisted public var userPreferredCoreID: String? - public dynamic var identifier: String = "" - - public override static func primaryKey() -> String? { - return "identifier" - } + @Persisted(primaryKey: true) public var identifier: String = "" // Hack to store controller layout because I don't want to make // all the complex objects it would require. Just store the plist dictionary data - internal dynamic var controlLayoutData: Data? + @Persisted internal dynamic var controlLayoutData: Data? public var controllerLayout: [ControlLayoutEntry]? { get { guard let controlLayoutData = controlLayoutData else { diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVUser.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVUser.swift index 039a4fd6d5..de98f3ae58 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVUser.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVUser.swift @@ -11,14 +11,14 @@ import RealmSwift @objcMembers public final class PVUser: Object { - public dynamic var uuid: String = "" - public dynamic var name: String = "" + @Persisted public var uuid: String = "" + @Persisted public var name: String = "" // Remote info - public dynamic var isPatron: Bool = false - public dynamic var savesAccess: Bool = false + @Persisted public var isPatron: Bool = false + @Persisted public var savesAccess: Bool = false - public dynamic var lastSeen: Date = Date() + @Persisted public var lastSeen: Date = Date() } // PVLibrary - Network diff --git a/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView+GamepadNavigation.swift b/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView+GamepadNavigation.swift index fdf93b2da1..17282e7047 100644 --- a/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView+GamepadNavigation.swift +++ b/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView+GamepadNavigation.swift @@ -10,10 +10,10 @@ extension ConsoleGamesView { internal var availableSections: [HomeSectionType] { [ - (showRecentSaveStates && !gamesViewModel.recentSaveStates.isEmpty) ? .recentSaveStates : nil, - (showFavorites && !gamesViewModel.favorites.isEmpty) ? .favorites : nil, - (showRecentGames && !gamesViewModel.recentlyPlayedGames.isEmpty) ? .recentlyPlayedGames : nil, - !gamesViewModel.games.isEmpty ? .allGames : nil + (showRecentSaveStates && !recentSaveStates.isEmpty) ? .recentSaveStates : nil, + (showFavorites && !favorites.isEmpty) ? .favorites : nil, + (showRecentGames && !recentlyPlayedGames.isEmpty) ? .recentlyPlayedGames : nil, + !games.isEmpty ? .allGames : nil ].compactMap { $0 } } @@ -28,7 +28,7 @@ extension ConsoleGamesView { switch section { case .recentSaveStates: - if let saveState = gamesViewModel.recentSaveStates.first(where: { $0.id == itemId }) { + if let saveState = recentSaveStates.first(where: { $0.id == itemId }) { Task.detached { @MainActor in await rootDelegate?.root_load( saveState.game, @@ -39,25 +39,25 @@ extension ConsoleGamesView { } } case .favorites: - if let game = gamesViewModel.favorites.first(where: { $0.id == itemId }) { + if let game = favorites.first(where: { $0.id == itemId }) { Task.detached { @MainActor in await rootDelegate?.root_load(game, sender: self, core: nil, saveState: nil) } } case .recentlyPlayedGames: - if let recentGame = gamesViewModel.recentlyPlayedGames.first(where: { $0.id == itemId })?.game { + if let recentGame = recentlyPlayedGames.first(where: { $0.id == itemId })?.game { Task.detached { @MainActor in await rootDelegate?.root_load(recentGame, sender: self, core: nil, saveState: nil) } } case .allGames: - if let game = gamesViewModel.games.first(where: { $0.id == itemId }) { + if let game = games.first(where: { $0.id == itemId }) { Task.detached { @MainActor in await rootDelegate?.root_load(game, sender: self, core: nil, saveState: nil) } } case .mostPlayed: - if let game = gamesViewModel.mostPlayed.first(where: { $0.id == itemId }) { + if let game = mostPlayed.first(where: { $0.id == itemId }) { Task.detached { @MainActor in await rootDelegate?.root_load(game, sender: self, core: nil, saveState: nil) } @@ -84,7 +84,7 @@ extension ConsoleGamesView { // Moving up to continues section - select last item gamesViewModel.updateFocus( section: nextSection, - item: gamesViewModel.recentSaveStates.last?.id + item: recentSaveStates.last?.id ) } else { // Any other section transition - select first item @@ -127,15 +127,15 @@ extension ConsoleGamesView { guard !game.isInvalidated else { return .allGames } // If we're in favorites section, ONLY return favorites if the game is actually in favorites if gamesViewModel.focusedSection == .favorites { - return gamesViewModel.favorites.contains(where: { $0.id == game.id }) ? .favorites : .allGames + return favorites.contains(where: { $0.id == game.id }) ? .favorites : .allGames } // If we're in recently played, ONLY return recently played if the game is actually in recently played else if gamesViewModel.focusedSection == .recentlyPlayedGames { - return gamesViewModel.recentlyPlayedGames.contains(where: { $0.game?.id == game.id }) ? .recentlyPlayedGames : .allGames + return recentlyPlayedGames.contains(where: { $0.game?.id == game.id }) ? .recentlyPlayedGames : .allGames } // If we're in most played, ONLY return most played if the game is actually in most played else if gamesViewModel.focusedSection == .mostPlayed { - return gamesViewModel.mostPlayed.contains(where: { $0.id == game.id }) ? .mostPlayed : .allGames + return mostPlayed.contains(where: { $0.id == game.id }) ? .mostPlayed : .allGames } // Default to all games else { @@ -193,7 +193,6 @@ extension ConsoleGamesView { private func handleVerticalNavigationWithinSection(_ section: HomeSectionType, direction: Float) { switch section { case .allGames: - let games = Array(gamesViewModel.games) if let currentIndex = games.firstIndex(where: { $0.id == gamesViewModel.focusedItemInSection }) { if direction > 0 { // Moving up @@ -295,15 +294,15 @@ extension ConsoleGamesView { private func getItemsForSection(_ section: HomeSectionType) -> [String] { switch section { case .recentSaveStates: - return gamesViewModel.recentSaveStates.map { $0.id } + return recentSaveStates.map { $0.id } case .favorites: - return gamesViewModel.favorites.map { $0.id } + return favorites.map { $0.id } case .recentlyPlayedGames: - return gamesViewModel.recentlyPlayedGames.map { $0.id } + return recentlyPlayedGames.map { $0.id } case .allGames: - return gamesViewModel.games.map { $0.id } + return games.map { $0.id } case .mostPlayed: - return gamesViewModel.mostPlayed.map { $0.id } + return mostPlayed.map { $0.id } } } } diff --git a/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView.swift b/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView.swift index 720ec135f6..3b71f8482b 100644 --- a/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView.swift +++ b/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView.swift @@ -66,6 +66,35 @@ struct ConsoleGamesView: SwiftUI.View { @State private var navigationTimer: Timer? @State private var initialDelay: TimeInterval = 0.5 @State private var repeatDelay: TimeInterval = 0.15 + + /// Note: these CANNOT be in a @StateObject + @ObservedResults( + PVGame.self, + filter: NSPredicate(format: "systemIdentifier == %@"), + sortDescriptor: SortDescriptor(keyPath: #keyPath(PVGame.title), ascending: false) + ) var games + + @ObservedResults( + PVSaveState.self, + filter: NSPredicate(format: "game.systemIdentifier == %@"), + sortDescriptor: SortDescriptor(keyPath: #keyPath(PVSaveState.date), ascending: false) + ) var recentSaveStates + + @ObservedResults( + PVRecentGame.self, + filter: NSPredicate(format: "game.systemIdentifier == %@") + ) var recentlyPlayedGames + + @ObservedResults( + PVGame.self, + filter: NSPredicate(format: "isFavorite == true AND systemIdentifier == %@") + ) var favorites + + @ObservedResults( + PVGame.self, + filter: NSPredicate(format: "systemIdentifier == %@ AND playCount > 0"), + sortDescriptor: SortDescriptor(keyPath: #keyPath(PVGame.playCount), ascending: false) + ) var mostPlayed private var sectionHeight: CGFloat { // Use compact size class to determine if we're in portrait on iPhone @@ -79,6 +108,30 @@ struct ConsoleGamesView: SwiftUI.View { self.viewModel = viewModel self.rootDelegate = rootDelegate self.gamesForSystemPredicate = NSPredicate(format: "systemIdentifier == %@", argumentArray: [console.identifier]) + + _games = ObservedResults( + PVGame.self, + filter: NSPredicate(format: "systemIdentifier == %@", console.identifier), + sortDescriptor: SortDescriptor(keyPath: #keyPath(PVGame.title), ascending: false) + ) + _recentSaveStates = ObservedResults( + PVSaveState.self, + filter: NSPredicate(format: "game.systemIdentifier == %@", console.identifier), + sortDescriptor: SortDescriptor(keyPath: #keyPath(PVSaveState.date), ascending: false) + ) + _recentlyPlayedGames = ObservedResults( + PVRecentGame.self, + filter: NSPredicate(format: "game.systemIdentifier == %@", console.identifier) + ) + _favorites = ObservedResults( + PVGame.self, + filter: NSPredicate(format: "isFavorite == true AND systemIdentifier == %@", console.identifier) + ) + _mostPlayed = ObservedResults( + PVGame.self, + filter: NSPredicate(format: "systemIdentifier == %@ AND playCount > 0", console.identifier), + sortDescriptor: SortDescriptor(keyPath: #keyPath(PVGame.playCount), ascending: false) + ) } var body: some SwiftUI.View { @@ -164,15 +217,15 @@ struct ConsoleGamesView: SwiftUI.View { // MARK: - Helper Methods private var hasRecentSaveStates: Bool { - !gamesViewModel.recentSaveStates.filter("game.systemIdentifier == %@", console.identifier).isEmpty + !recentSaveStates.filter("game.systemIdentifier == %@", console.identifier).isEmpty } private var hasFavorites: Bool { - !gamesViewModel.favorites.filter("systemIdentifier == %@", console.identifier).isEmpty + !favorites.filter("systemIdentifier == %@", console.identifier).isEmpty } private var hasRecentlyPlayedGames: Bool { - !gamesViewModel.recentlyPlayedGames.isEmpty + !recentlyPlayedGames.isEmpty } private func loadGame(_ game: PVGame) { @@ -189,7 +242,7 @@ struct ConsoleGamesView: SwiftUI.View { if AppState.shared.isSimulator { count = max(0,roundedScale ) } else { - count = min(max(0, roundedScale), gamesViewModel.games.count) + count = min(max(0, roundedScale), games.count) } return count } @@ -419,7 +472,7 @@ extension ConsoleGamesView { @ViewBuilder private func continueSection() -> some View { Group { - if showRecentSaveStates && !gamesViewModel.recentSaveStates.isEmpty { + if showRecentSaveStates && !recentSaveStates.isEmpty { HomeContinueSection( rootDelegate: rootDelegate, consoleIdentifier: console.identifier, @@ -440,9 +493,9 @@ extension ConsoleGamesView { @ViewBuilder private func favoritesSection() -> some View { Group { - if showFavorites && !gamesViewModel.favorites.isEmpty { + if showFavorites && !favorites.isEmpty { HomeSection(title: "Favorites") { - ForEach(gamesViewModel.favorites, id: \.self) { game in + ForEach(favorites, id: \.self) { game in gameItem(game, section: .favorites) } } @@ -455,9 +508,9 @@ extension ConsoleGamesView { @ViewBuilder private func recentlyPlayedSection() -> some View { Group { - if showRecentGames && !gamesViewModel.recentlyPlayedGames.isEmpty { + if showRecentGames && !recentlyPlayedGames.isEmpty { HomeSection(title: "Recently Played") { - ForEach(gamesViewModel.recentlyPlayedGames, id: \.self) { recentGame in + ForEach(recentlyPlayedGames, id: \.self) { recentGame in if let game = recentGame.game { gameItem(game, section: .recentlyPlayedGames) } @@ -472,7 +525,7 @@ extension ConsoleGamesView { @ViewBuilder private func gamesSection() -> some View { Group { - if gamesViewModel.games.filter{!$0.isInvalidated}.isEmpty && AppState.shared.isSimulator { + if games.filter{!$0.isInvalidated}.isEmpty && AppState.shared.isSimulator { let fakeGames = PVGame.mockGenerate(systemID: console.identifier) if viewModel.viewGamesAsGrid { showGamesGrid(fakeGames) @@ -486,9 +539,9 @@ extension ConsoleGamesView { .foregroundColor(themeManager.currentPalette.gameLibraryText.swiftUIColor) if viewModel.viewGamesAsGrid { - showGamesGrid(gamesViewModel.games) + showGamesGrid(games) } else { - showGamesList(gamesViewModel.games) + showGamesList(games) } } } @@ -539,7 +592,6 @@ extension ConsoleGamesView { if !saveState.isInvalidated && !saveState.game.isInvalidated { GameItemView( game: saveState.game, - saveState: saveState, constrainHeight: true, viewType: .cell, sectionContext: .recentSaveStates, diff --git a/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesViewModel.swift b/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesViewModel.swift index 805e3188a8..0998f44597 100644 --- a/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesViewModel.swift +++ b/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesViewModel.swift @@ -17,62 +17,11 @@ import Combine class ConsoleGamesViewModel: ObservableObject { let console: PVSystem - @ObservedResults( - PVGame.self, - filter: NSPredicate(format: "systemIdentifier == %@"), - sortDescriptor: SortDescriptor(keyPath: #keyPath(PVGame.title), ascending: false) - ) var games - - @ObservedResults( - PVSaveState.self, - filter: NSPredicate(format: "game.systemIdentifier == %@"), - sortDescriptor: SortDescriptor(keyPath: #keyPath(PVSaveState.date), ascending: false) - ) var recentSaveStates - - @ObservedResults( - PVRecentGame.self, - filter: NSPredicate(format: "game.systemIdentifier == %@") - ) var recentlyPlayedGames - - @ObservedResults( - PVGame.self, - filter: NSPredicate(format: "isFavorite == true AND systemIdentifier == %@") - ) var favorites - - @ObservedResults( - PVGame.self, - filter: NSPredicate(format: "systemIdentifier == %@ AND playCount > 0"), - sortDescriptor: SortDescriptor(keyPath: #keyPath(PVGame.playCount), ascending: false) - ) var mostPlayed - @Published var focusedSection: HomeSectionType? @Published var focusedItemInSection: String? init(console: PVSystem) { self.console = console - _games = ObservedResults( - PVGame.self, - filter: NSPredicate(format: "systemIdentifier == %@", console.identifier), - sortDescriptor: SortDescriptor(keyPath: #keyPath(PVGame.title), ascending: false) - ) - _recentSaveStates = ObservedResults( - PVSaveState.self, - filter: NSPredicate(format: "game.systemIdentifier == %@", console.identifier), - sortDescriptor: SortDescriptor(keyPath: #keyPath(PVSaveState.date), ascending: false) - ) - _recentlyPlayedGames = ObservedResults( - PVRecentGame.self, - filter: NSPredicate(format: "game.systemIdentifier == %@", console.identifier) - ) - _favorites = ObservedResults( - PVGame.self, - filter: NSPredicate(format: "isFavorite == true AND systemIdentifier == %@", console.identifier) - ) - _mostPlayed = ObservedResults( - PVGame.self, - filter: NSPredicate(format: "systemIdentifier == %@ AND playCount > 0", console.identifier), - sortDescriptor: SortDescriptor(keyPath: #keyPath(PVGame.playCount), ascending: false) - ) } // Navigation state helpers diff --git a/PVUI/Sources/PVSwiftUI/GameItem/GameItemView.swift b/PVUI/Sources/PVSwiftUI/GameItem/GameItemView.swift index 6baf7ccb60..e07cadd225 100644 --- a/PVUI/Sources/PVSwiftUI/GameItem/GameItemView.swift +++ b/PVUI/Sources/PVSwiftUI/GameItem/GameItemView.swift @@ -15,7 +15,6 @@ import PVThemes struct GameItemView: SwiftUI.View { @ObservedRealmObject var game: PVGame - var saveState: PVSaveState? var constrainHeight: Bool = false var viewType: GameItemViewType = .cell /// The section context this GameItemView is being rendered in