From 093b565b3b6276a39e3fe4dde71d614cafa0bbf7 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 32d6ac7b..5f221fca 100644 --- a/AltStoreCore/Model/DatabaseManager.swift +++ b/AltStoreCore/Model/DatabaseManager.swift @@ -180,6 +180,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 @@ -374,7 +417,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 0d493ec8..050ca8f3 100644 --- a/AltStoreCore/Model/Source.swift +++ b/AltStoreCore/Model/Source.swift @@ -75,6 +75,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]? @@ -210,6 +212,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 58ecec44..5a72bde2 100644 --- a/AltStoreCore/Model/StoreApp.swift +++ b/AltStoreCore/Model/StoreApp.swift @@ -73,6 +73,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 { @@ -330,6 +331,13 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable throw error } } + + public override func awakeFromInsert() + { + super.awakeFromInsert() + + self.featuredSortID = UUID().uuidString + } } internal extension StoreApp