Allows changing BrowseViewController sort order

This commit is contained in:
Riley Testut
2023-12-07 17:54:59 -06:00
committed by Magesh K
parent 5cb40de113
commit 71eb77cfda
4 changed files with 150 additions and 7 deletions

View File

@@ -439,6 +439,7 @@
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */; };
D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
D5DB145B28F9DC5C00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
D5DB81642B0410BC003F5F8B /* AppSorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB81632B0410BC003F5F8B /* AppSorting.swift */; };
D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */; };
D5E3FB9828FDFAD90034B72C /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.swift */; };
D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */; };
@@ -1115,6 +1116,7 @@
D5CF56812A0D83F9006D93E2 /* VerificationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationError.swift; sourceTree = "<group>"; };
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = "<group>"; };
D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTLocalizedError.swift; sourceTree = "<group>"; };
D5DB81632B0410BC003F5F8B /* AppSorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSorting.swift; sourceTree = "<group>"; };
D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateKnownSourcesOperation.swift; sourceTree = "<group>"; };
D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchViewController.swift; sourceTree = "<group>"; };
D5F48B4729CCF21B002B52A4 /* AltStore+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AltStore+Async.swift"; sourceTree = "<group>"; };
@@ -1654,6 +1656,7 @@
BFB39B5B252BC10E00D1BE50 /* Managed.swift */,
D5F48B4929CD0B67002B52A4 /* AsyncManaged.swift */,
D5893F812A141E4900E767CD /* KnownSource.swift */,
D5DB81632B0410BC003F5F8B /* AppSorting.swift */,
BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */,
BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */,
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitID.h */,
@@ -3158,6 +3161,7 @@
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */,
D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */,
BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */,
D5DB81642B0410BC003F5F8B /* AppSorting.swift in Sources */,
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitID.m in Sources */,
BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */,
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */,

View File

@@ -25,6 +25,9 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
private let prototypeCell = AppCardCollectionViewCell(frame: .zero)
private var sortButton: UIBarButtonItem?
private var preferredAppSorting: AppSorting = UserDefaults.shared.preferredAppSorting
private var cancellables = Set<AnyCancellable>()
init?(source: Source?, coder: NSCoder)
@@ -94,8 +97,14 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
self.navigationItem.preferredSearchBarPlacement = .inline
}
if #available(iOS 15, *)
{
self.prepareAppSorting()
}
self.preparePipeline()
self.updateDataSource()
self.update()
}
@@ -119,13 +128,9 @@ private extension BrowseViewController
.store(in: &self.cancellables)
}
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
func makeFetchRequest() -> NSFetchRequest<StoreApp>
{
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.name, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
let predicate = StoreApp.visibleAppsPredicate
@@ -140,6 +145,38 @@ private extension BrowseViewController
fetchRequest.predicate = predicate
}
var sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.name, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true)]
switch self.preferredAppSorting
{
case .default:
let descriptor = NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: self.preferredAppSorting.isAscending)
sortDescriptors.insert(descriptor, at: 0)
case .name:
// Already sorting by name, no need to prepend additional sort descriptor.
break
case .developer:
let descriptor = NSSortDescriptor(keyPath: \StoreApp.developerName, ascending: self.preferredAppSorting.isAscending)
sortDescriptors.insert(descriptor, at: 0)
case .lastUpdated:
let descriptor = NSSortDescriptor(keyPath: \StoreApp.latestSupportedVersion?.date, ascending: self.preferredAppSorting.isAscending)
sortDescriptors.insert(descriptor, at: 0)
}
fetchRequest.sortDescriptors = sortDescriptors
return fetchRequest
}
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
{
let fetchRequest = self.makeFetchRequest()
let context = self.source?.managedObjectContext ?? DatabaseManager.shared.viewContext
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: context)
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
@@ -192,7 +229,12 @@ private extension BrowseViewController
func updateDataSource()
{
self.dataSource.predicate = nil
self.dataSource.predicate = nil
let fetchRequest = self.makeFetchRequest()
let context = self.source?.managedObjectContext ?? DatabaseManager.shared.viewContext
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
self.dataSource.fetchedResultsController = fetchedResultsController
}
func updateSources()
@@ -239,6 +281,52 @@ private extension BrowseViewController
self.placeholderView.activityIndicatorView.stopAnimating()
}
@available(iOS 15, *)
func prepareAppSorting()
{
if self.preferredAppSorting == .default && self.source == nil
{
// Only allow `default` sorting if source is non-nil.
// Otherwise, fall back to `lastUpdated` sorting.
self.preferredAppSorting = .lastUpdated
// Don't update UserDefaults unless explicitly changed by user.
// UserDefaults.shared.preferredAppSorting = .lastUpdated
}
let children = UIDeferredMenuElement.uncached { [weak self] completion in
guard let self else { return completion([]) }
var sortingOptions = AppSorting.allCases
if self.source == nil
{
// Only allow `default` sorting when source is non-nil.
sortingOptions = sortingOptions.filter { $0 != .default }
}
let actions = sortingOptions.map { sorting in
let state: UIMenuElement.State = (sorting == self.preferredAppSorting) ? .on : .off
let action = UIAction(title: sorting.localizedName, image: nil, state: state) { action in
self.preferredAppSorting = sorting
UserDefaults.shared.preferredAppSorting = sorting // Update separately to save change.
self.updateDataSource()
}
return action
}
completion(actions)
}
let sortMenu = UIMenu(title: NSLocalizedString("Sort by…", comment: ""), options: [.singleSelection], children: [children])
let sortIcon = UIImage(systemName: "arrow.up.arrow.down")
let sortButton = UIBarButtonItem(title: NSLocalizedString("Sort by…", comment: ""), image: sortIcon, primaryAction: nil, menu: sortMenu)
self.sortButton = sortButton
self.navigationItem.rightBarButtonItems = [sortButton, .flexibleSpace()] // flexibleSpace() required to prevent showing full search bar inline.
}
}

View File

@@ -53,6 +53,17 @@ public extension UserDefaults
@NSManaged var trustedServerURL: String?
@NSManaged var skipPatreonDownloads: Bool
@nonobjc var preferredAppSorting: AppSorting {
get {
let sorting = _preferredAppSorting.flatMap { AppSorting(rawValue: $0) } ?? .default
return sorting
}
set {
_preferredAppSorting = newValue.rawValue
}
}
@NSManaged @objc(preferredAppSorting) private var _preferredAppSorting: String?
@nonobjc
var activeAppsLimit: Int? {
get {
@@ -103,6 +114,10 @@ public extension UserDefaults
let permissionCheckingDisabled = false
#endif
// Pre-iOS 15 doesn't support custom sorting, so default to sorting by name.
// Otherwise, default to `default` sorting (a.k.a. "source order").
let preferredAppSorting: AppSorting = if #available(iOS 15, *) { .default } else { .name }
let defaults = [
#keyPath(UserDefaults.isAppLimitDisabled): false,
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
@@ -117,7 +132,8 @@ public extension UserDefaults
#keyPath(UserDefaults.ignoreActiveAppsLimit): false,
#keyPath(UserDefaults.isMacDirtyCowSupported): isMacDirtyCowSupported
#keyPath(UserDefaults.permissionCheckingDisabled): permissionCheckingDisabled,
] as [String : Any]
#keyPath(UserDefaults._preferredAppSorting): preferredAppSorting.rawValue,
] as [String: Any]
UserDefaults.standard.register(defaults: defaults)
UserDefaults.shared.register(defaults: defaults)

View File

@@ -0,0 +1,35 @@
//
// AppSorting.swift
// AltStoreCore
//
// Created by Riley Testut on 11/14/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import Foundation
public enum AppSorting: String, CaseIterable
{
case `default`
case name
case developer
case lastUpdated
public var localizedName: String {
switch self
{
case .default: return NSLocalizedString("Default", comment: "")
case .name: return NSLocalizedString("Name", comment: "")
case .developer: return NSLocalizedString("Developer", comment: "")
case .lastUpdated: return NSLocalizedString("Last Updated", comment: "")
}
}
public var isAscending: Bool {
switch self
{
case .default, .name, .developer: return true
case .lastUpdated: return false
}
}
}