From 9ea94912d49516e016c212cbb7935d19c047593f Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 8 Dec 2023 14:32:57 -0600 Subject: [PATCH] Randomizes featured source + app order at app launch --- AltStore/Browse/FeaturedViewController.swift | 10 ++-- .../AltStore 14.xcdatamodel/contents | 2 + AltStoreCore/Model/DatabaseManager.swift | 49 ++++++++++++++++++- AltStoreCore/Model/MergePolicy.swift | 12 +++++ AltStoreCore/Model/Source.swift | 9 ++++ AltStoreCore/Model/StoreApp.swift | 8 +++ 6 files changed, 84 insertions(+), 6 deletions(-) diff --git a/AltStore/Browse/FeaturedViewController.swift b/AltStore/Browse/FeaturedViewController.swift index 96a9e4f7..1d751b91 100644 --- a/AltStore/Browse/FeaturedViewController.swift +++ b/AltStore/Browse/FeaturedViewController.swift @@ -312,7 +312,7 @@ private extension FeaturedViewController fetchRequest.returnsObjectsAsFaults = false fetchRequest.sortDescriptors = [ // Sort by Source first to group into sections. - NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true), + NSSortDescriptor(keyPath: \StoreApp._source?.featuredSortID, ascending: true), // Show uninstalled apps first. // Sorting by StoreApp.installedApp crashes because InstalledApp does not respond to compare: @@ -324,8 +324,8 @@ private extension FeaturedViewController // Instead, sort by StoreApp.featuringSource.identifier, which will be either nil OR source ID. NSSortDescriptor(keyPath: \StoreApp.featuringSource?.identifier, ascending: false), - // Sort by name. - NSSortDescriptor(keyPath: \StoreApp.name, ascending: true), + // Randomize order within sections. + NSSortDescriptor(keyPath: \StoreApp.featuredSortID, ascending: true), // Sanity check to ensure stable ordering NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true) @@ -346,14 +346,14 @@ private extension FeaturedViewController let primaryFetchRequest = fetchRequest.copy() as! NSFetchRequest primaryFetchRequest.predicate = sourceHasRemainingAppsPredicate - let primaryController = NSFetchedResultsController(fetchRequest: primaryFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(StoreApp.sourceIdentifier), cacheName: nil) + let primaryController = NSFetchedResultsController(fetchRequest: primaryFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(StoreApp._source.featuredSortID), cacheName: nil) let primaryDataSource = RSTFetchedResultsCollectionViewDataSource(fetchedResultsController: primaryController) primaryDataSource.liveFetchLimit = 5 let secondaryFetchRequest = fetchRequest.copy() as! NSFetchRequest secondaryFetchRequest.predicate = NSCompoundPredicate(notPredicateWithSubpredicate: sourceHasRemainingAppsPredicate) - let secondaryController = NSFetchedResultsController(fetchRequest: secondaryFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(StoreApp.sourceIdentifier), cacheName: nil) + let secondaryController = NSFetchedResultsController(fetchRequest: secondaryFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(StoreApp._source.featuredSortID), cacheName: nil) let secondaryDataSource = RSTFetchedResultsCollectionViewDataSource(fetchedResultsController: secondaryController) secondaryDataSource.liveFetchLimit = 5 diff --git a/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 14.xcdatamodel/contents b/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 14.xcdatamodel/contents index 51c245cf..3367fcee 100644 --- a/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 14.xcdatamodel/contents +++ b/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 14.xcdatamodel/contents @@ -217,6 +217,7 @@ + @@ -242,6 +243,7 @@ + diff --git a/AltStoreCore/Model/DatabaseManager.swift b/AltStoreCore/Model/DatabaseManager.swift index 2c8111a3..9a72ea53 100644 --- a/AltStoreCore/Model/DatabaseManager.swift +++ b/AltStoreCore/Model/DatabaseManager.swift @@ -178,6 +178,49 @@ public extension DatabaseManager } } } + + func updateFeaturedSortIDs() async + { + let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() + context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy // DON'T use our custom merge policy, because that one ignores changes to featuredSortID. + await context.performAsync { + do + { + // Randomize source order + let fetchRequest = Source.fetchRequest() + let sources = try context.fetch(fetchRequest) + + for source in sources + { + source.featuredSortID = UUID().uuidString + } + + try context.save() + } + catch + { + Logger.main.error("Failed to update source order. \(error.localizedDescription, privacy: .public)") + } + + do + { + // Randomize app order + let fetchRequest = StoreApp.fetchRequest() + let apps = try context.fetch(fetchRequest) + + for app in apps + { + app.featuredSortID = UUID().uuidString + } + + try context.save() + } + catch + { + Logger.main.error("Failed to update app order. \(error.localizedDescription, privacy: .public)") + } + } + } } public extension DatabaseManager @@ -372,7 +415,11 @@ private extension DatabaseManager do { try context.save() - completionHandler(.success(())) + + Task(priority: .high) { + await self.updateFeaturedSortIDs() + completionHandler(.success(())) + } } catch { diff --git a/AltStoreCore/Model/MergePolicy.swift b/AltStoreCore/Model/MergePolicy.swift index 4e6f0f4c..eea54b0e 100644 --- a/AltStoreCore/Model/MergePolicy.swift +++ b/AltStoreCore/Model/MergePolicy.swift @@ -234,6 +234,12 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy sortedScreenshotIDsByGlobalAppID[globallyUniqueID] = contextScreenshotIDs } + // Revert contextApp.featuredSortID to database value (if it exists). + if let featuredSortID = databaseObject.featuredSortID + { + contextApp.featuredSortID = featuredSortID + } + case let databaseObject as Source: guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break } @@ -263,6 +269,12 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy featuredAppIDsBySourceID[databaseObject.identifier] = contextSource.featuredApps?.map { $0.bundleIdentifier } } + // Revert conflictedObject.featuredSortID to database value (if it exists). + if let featuredSortID = databaseObject.featuredSortID + { + conflictedObject.featuredSortID = featuredSortID + } + case let databasePledge as Pledge: guard let contextPledge = conflict.conflictingObjects.first as? Pledge else { break } diff --git a/AltStoreCore/Model/Source.swift b/AltStoreCore/Model/Source.swift index 897dfe44..88f031fb 100644 --- a/AltStoreCore/Model/Source.swift +++ b/AltStoreCore/Model/Source.swift @@ -215,6 +215,8 @@ public class Source: NSManagedObject, Fetchable, Decodable @NSManaged public var error: NSError? + @NSManaged public var featuredSortID: String? + /* Non-Core Data Properties */ public var userInfo: [ALTSourceUserInfoKey: String]? @@ -350,6 +352,13 @@ public class Source: NSManagedObject, Fetchable, Decodable throw error } } + + public override func awakeFromInsert() + { + super.awakeFromInsert() + + self.featuredSortID = UUID().uuidString + } } public extension Source diff --git a/AltStoreCore/Model/StoreApp.swift b/AltStoreCore/Model/StoreApp.swift index 6c1a6947..f6f5032a 100644 --- a/AltStoreCore/Model/StoreApp.swift +++ b/AltStoreCore/Model/StoreApp.swift @@ -141,6 +141,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable @NSManaged @objc(pledgeAmount) private var _pledgeAmount: NSDecimalNumber? @NSManaged public var sortIndex: Int32 + @NSManaged public var featuredSortID: String? @objc public internal(set) var sourceIdentifier: String? { get { @@ -463,6 +464,13 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable throw error } } + + public override func awakeFromInsert() + { + super.awakeFromInsert() + + self.featuredSortID = UUID().uuidString + } } internal extension StoreApp