Randomizes featured source + app order at app launch

This commit is contained in:
Riley Testut
2023-12-08 14:32:57 -06:00
committed by Magesh K
parent 36743c0cf4
commit 9ea94912d4
6 changed files with 84 additions and 6 deletions

View File

@@ -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<StoreApp>
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<StoreApp>(fetchedResultsController: primaryController)
primaryDataSource.liveFetchLimit = 5
let secondaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp>
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<StoreApp>(fetchedResultsController: secondaryController)
secondaryDataSource.liveFetchLimit = 5

View File

@@ -217,6 +217,7 @@
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
<attribute name="hasFeaturedApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="headerImageURL" optional="YES" attributeType="URI"/>
<attribute name="iconURL" optional="YES" attributeType="URI"/>
@@ -242,6 +243,7 @@
<attribute name="category" optional="YES" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isHiddenWithoutPledge" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>

View File

@@ -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
{

View File

@@ -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 }

View File

@@ -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

View File

@@ -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