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.returnsObjectsAsFaults = false
fetchRequest.sortDescriptors = [ fetchRequest.sortDescriptors = [
// Sort by Source first to group into sections. // 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. // Show uninstalled apps first.
// Sorting by StoreApp.installedApp crashes because InstalledApp does not respond to compare: // 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. // Instead, sort by StoreApp.featuringSource.identifier, which will be either nil OR source ID.
NSSortDescriptor(keyPath: \StoreApp.featuringSource?.identifier, ascending: false), NSSortDescriptor(keyPath: \StoreApp.featuringSource?.identifier, ascending: false),
// Sort by name. // Randomize order within sections.
NSSortDescriptor(keyPath: \StoreApp.name, ascending: true), NSSortDescriptor(keyPath: \StoreApp.featuredSortID, ascending: true),
// Sanity check to ensure stable ordering // Sanity check to ensure stable ordering
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true) NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)
@@ -346,14 +346,14 @@ private extension FeaturedViewController
let primaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp> let primaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp>
primaryFetchRequest.predicate = sourceHasRemainingAppsPredicate 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) let primaryDataSource = RSTFetchedResultsCollectionViewDataSource<StoreApp>(fetchedResultsController: primaryController)
primaryDataSource.liveFetchLimit = 5 primaryDataSource.liveFetchLimit = 5
let secondaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp> let secondaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp>
secondaryFetchRequest.predicate = NSCompoundPredicate(notPredicateWithSubpredicate: sourceHasRemainingAppsPredicate) 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) let secondaryDataSource = RSTFetchedResultsCollectionViewDataSource<StoreApp>(fetchedResultsController: secondaryController)
secondaryDataSource.liveFetchLimit = 5 secondaryDataSource.liveFetchLimit = 5

View File

@@ -217,6 +217,7 @@
</entity> </entity>
<entity name="Source" representedClassName="Source" syncable="YES"> <entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/> <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="hasFeaturedApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="headerImageURL" optional="YES" attributeType="URI"/> <attribute name="headerImageURL" optional="YES" attributeType="URI"/>
<attribute name="iconURL" optional="YES" attributeType="URI"/> <attribute name="iconURL" optional="YES" attributeType="URI"/>
@@ -242,6 +243,7 @@
<attribute name="category" optional="YES" attributeType="String"/> <attribute name="category" optional="YES" attributeType="String"/>
<attribute name="developerName" attributeType="String"/> <attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/> <attribute name="downloadURL" attributeType="URI"/>
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
<attribute name="iconURL" attributeType="URI"/> <attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/> <attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isHiddenWithoutPledge" 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 public extension DatabaseManager
@@ -372,8 +415,12 @@ private extension DatabaseManager
do do
{ {
try context.save() try context.save()
Task(priority: .high) {
await self.updateFeaturedSortIDs()
completionHandler(.success(())) completionHandler(.success(()))
} }
}
catch catch
{ {
completionHandler(.failure(error)) completionHandler(.failure(error))

View File

@@ -234,6 +234,12 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
sortedScreenshotIDsByGlobalAppID[globallyUniqueID] = contextScreenshotIDs 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: case let databaseObject as Source:
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break } 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 } 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: case let databasePledge as Pledge:
guard let contextPledge = conflict.conflictingObjects.first as? Pledge else { break } 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 error: NSError?
@NSManaged public var featuredSortID: String?
/* Non-Core Data Properties */ /* Non-Core Data Properties */
public var userInfo: [ALTSourceUserInfoKey: String]? public var userInfo: [ALTSourceUserInfoKey: String]?
@@ -350,6 +352,13 @@ public class Source: NSManagedObject, Fetchable, Decodable
throw error throw error
} }
} }
public override func awakeFromInsert()
{
super.awakeFromInsert()
self.featuredSortID = UUID().uuidString
}
} }
public extension Source public extension Source

View File

@@ -141,6 +141,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
@NSManaged @objc(pledgeAmount) private var _pledgeAmount: NSDecimalNumber? @NSManaged @objc(pledgeAmount) private var _pledgeAmount: NSDecimalNumber?
@NSManaged public var sortIndex: Int32 @NSManaged public var sortIndex: Int32
@NSManaged public var featuredSortID: String?
@objc public internal(set) var sourceIdentifier: String? { @objc public internal(set) var sourceIdentifier: String? {
get { get {
@@ -463,6 +464,13 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
throw error throw error
} }
} }
public override func awakeFromInsert()
{
super.awakeFromInsert()
self.featuredSortID = UUID().uuidString
}
} }
internal extension StoreApp internal extension StoreApp