diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard
index b5181c5b..de97007b 100644
--- a/AltStore/Base.lproj/Main.storyboard
+++ b/AltStore/Base.lproj/Main.storyboard
@@ -603,6 +603,7 @@ World
+
@@ -871,6 +872,28 @@ World
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift
index 1f261dfe..4a38b1b6 100644
--- a/AltStore/My Apps/MyAppsViewController.swift
+++ b/AltStore/My Apps/MyAppsViewController.swift
@@ -17,6 +17,7 @@ extension MyAppsViewController
{
private enum Section: Int, CaseIterable
{
+ case noUpdates
case updates
case installedApps
}
@@ -37,6 +38,7 @@ private extension Date
class MyAppsViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
+ private lazy var noUpdatesDataSource = self.makeNoUpdatesDataSource()
private lazy var updatesDataSource = self.makeUpdatesDataSource()
private lazy var installedAppsDataSource = self.makeInstalledAppsDataSource()
@@ -69,6 +71,9 @@ class MyAppsViewController: UICollectionViewController
{
super.viewDidLoad()
+ // Allows us to intercept delegate callbacks.
+ self.updatesDataSource.fetchedResultsController.delegate = self
+
self.collectionView.dataSource = self.dataSource
self.collectionView.prefetchDataSource = self.dataSource
@@ -97,11 +102,26 @@ private extension MyAppsViewController
{
func makeDataSource() -> RSTCompositeCollectionViewPrefetchingDataSource
{
- let dataSource = RSTCompositeCollectionViewPrefetchingDataSource(dataSources: [self.updatesDataSource, self.installedAppsDataSource])
+ let dataSource = RSTCompositeCollectionViewPrefetchingDataSource(dataSources: [self.noUpdatesDataSource, self.updatesDataSource, self.installedAppsDataSource])
dataSource.proxy = self
return dataSource
}
+ func makeNoUpdatesDataSource() -> RSTDynamicCollectionViewPrefetchingDataSource
+ {
+ let dynamicDataSource = RSTDynamicCollectionViewPrefetchingDataSource()
+ dynamicDataSource.numberOfSectionsHandler = { 1 }
+ dynamicDataSource.numberOfItemsHandler = { _ in self.updatesDataSource.itemCount == 0 ? 1 : 0 }
+ dynamicDataSource.cellIdentifierHandler = { _ in "NoUpdatesCell" }
+ dynamicDataSource.cellConfigurationHandler = { (cell, _, indexPath) in
+ cell.layer.cornerRadius = 20
+ cell.layer.masksToBounds = true
+ cell.contentView.backgroundColor = UIColor.altGreen.withAlphaComponent(0.15)
+ }
+
+ return dynamicDataSource
+ }
+
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource
{
let fetchRequest = InstalledApp.updatesFetchRequest()
@@ -232,6 +252,10 @@ private extension MyAppsViewController
self.navigationController?.tabBarItem.badgeValue = nil
UIApplication.shared.applicationIconBadgeNumber = 0
}
+
+ UIView.performWithoutAnimation {
+ self.collectionView.reloadSections(IndexSet(integer: Section.updates.rawValue))
+ }
}
func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping (Result<[String : Result], Error>) -> Void)
@@ -282,7 +306,9 @@ private extension MyAppsViewController
self.refreshGroup = group
- self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue))
+ UIView.performWithoutAnimation {
+ self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue))
+ }
}
if installedApps.contains(where: { $0.app.identifier == App.altstoreAppID })
@@ -382,9 +408,7 @@ private extension MyAppsViewController
}
self.refresh([installedApp]) { (result) in
- DispatchQueue.main.async {
- self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue))
- }
+ print("Finished refreshing with result:", result.error?.localizedDescription ?? "success")
}
}
@@ -433,6 +457,8 @@ private extension MyAppsViewController
print("Updated app:", app.identifier)
// No need to reload, since the the update cell is gone now.
}
+
+ self.update()
}
}
@@ -468,6 +494,17 @@ extension MyAppsViewController
headerView.button.setTitleColor(.altGreen, for: .normal)
headerView.button.addTarget(self, action: #selector(MyAppsViewController.toggleAppUpdates), for: .primaryActionTriggered)
+ if self.isUpdateSectionCollapsed
+ {
+ headerView.button.titleLabel?.transform = .identity
+ }
+ else
+ {
+ headerView.button.titleLabel?.transform = CGAffineTransform.identity.rotated(by: .pi)
+ }
+
+ headerView.isHidden = (self.updatesDataSource.itemCount <= 2)
+
headerView.button.layoutIfNeeded()
}
@@ -498,9 +535,16 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
+ let padding = 30 as CGFloat
+ let width = collectionView.bounds.width - padding
+
let section = Section.allCases[indexPath.section]
switch section
{
+ case .noUpdates:
+ let size = CGSize(width: width, height: 44)
+ return size
+
case .updates:
let item = self.dataSource.item(at: indexPath)
@@ -509,9 +553,6 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
return previousHeight
}
- let padding = 30 as CGFloat
- let width = collectionView.bounds.width - padding
-
let widthConstraint = self.prototypeUpdateCell.contentView.widthAnchor.constraint(equalToConstant: width)
NSLayoutConstraint.activate([widthConstraint])
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
@@ -532,7 +573,11 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
let section = Section.allCases[section]
switch section
{
- case .updates: return CGSize(width: collectionView.bounds.width, height: 26)
+ case .noUpdates: return .zero
+ case .updates:
+ let height: CGFloat = self.updatesDataSource.itemCount > maximumCollapsedUpdatesCount ? 26 : 0
+ return CGSize(width: collectionView.bounds.width, height: height)
+
case .installedApps: return CGSize(width: collectionView.bounds.width, height: 29)
}
}
@@ -542,8 +587,54 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
let section = Section.allCases[section]
switch section
{
- case .updates: return UIEdgeInsets(top: 12, left: 15, bottom: 20, right: 15)
- case .installedApps: return UIEdgeInsets(top: 13, left: 0, bottom: 20, right: 0)
+ case .noUpdates:
+ guard self.updatesDataSource.itemCount == 0 else { return .zero }
+ return UIEdgeInsets(top: 12, left: 15, bottom: 20, right: 15)
+
+ case .updates:
+ guard self.updatesDataSource.itemCount > 0 else { return .zero }
+ return UIEdgeInsets(top: 12, left: 15, bottom: 20, right: 15)
+
+ case .installedApps: return UIEdgeInsets(top: 12, left: 0, bottom: 20, right: 0)
}
}
}
+
+extension MyAppsViewController: NSFetchedResultsControllerDelegate
+{
+ func controllerWillChangeContent(_ controller: NSFetchedResultsController)
+ {
+ self.updatesDataSource.controllerWillChangeContent(controller)
+ }
+
+ func controller(_ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType)
+ {
+ self.updatesDataSource.controller(controller, didChange: sectionInfo, atSectionIndex: UInt(sectionIndex), for: type)
+ }
+
+ func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?)
+ {
+ self.updatesDataSource.controller(controller, didChange: anObject, at: indexPath, for: type, newIndexPath: newIndexPath)
+ }
+
+ func controllerDidChangeContent(_ controller: NSFetchedResultsController)
+ {
+ let previousUpdateCount = self.collectionView.numberOfItems(inSection: Section.updates.rawValue)
+ let updateCount = Int(self.updatesDataSource.itemCount)
+
+ if previousUpdateCount == 0 && updateCount > 0
+ {
+ // Remove "No Updates Available" cell.
+ let change = RSTCellContentChange(type: .delete, currentIndexPath: IndexPath(item: 0, section: Section.noUpdates.rawValue), destinationIndexPath: nil)
+ self.collectionView.add(change)
+ }
+ else if previousUpdateCount > 0 && updateCount == 0
+ {
+ // Insert "No Updates Available" cell.
+ let change = RSTCellContentChange(type: .insert, currentIndexPath: nil, destinationIndexPath: IndexPath(item: 0, section: Section.noUpdates.rawValue))
+ self.collectionView.add(change)
+ }
+
+ self.updatesDataSource.controllerDidChangeContent(controller)
+ }
+}