2019-05-20 21:26:01 +02:00
//
// M y A p p s V i e w C o n t r o l l e r . s w i f t
// A l t S t o r e
//
2019-07-19 16:42:40 -07:00
// C r e a t e d b y R i l e y T e s t u t o n 7 / 1 6 / 1 9 .
2019-05-20 21:26:01 +02:00
// C o p y r i g h t © 2 0 1 9 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import UIKit
2019-07-19 16:42:40 -07:00
import AltKit
2019-05-20 21:26:01 +02:00
import Roxas
2019-07-28 15:51:36 -07:00
import AltSign
2019-08-20 19:06:03 -05:00
import Nuke
2019-07-19 16:42:40 -07:00
private let maximumCollapsedUpdatesCount = 2
2019-06-10 15:03:47 -07:00
2019-07-19 16:42:40 -07:00
extension MyAppsViewController
{
private enum Section : Int , CaseIterable
{
2019-07-24 15:01:33 -07:00
case noUpdates
2019-07-19 16:42:40 -07:00
case updates
case installedApps
}
}
class MyAppsViewController : UICollectionViewController
2019-05-20 21:26:01 +02:00
{
private lazy var dataSource = self . makeDataSource ( )
2019-07-24 15:01:33 -07:00
private lazy var noUpdatesDataSource = self . makeNoUpdatesDataSource ( )
2019-07-19 16:42:40 -07:00
private lazy var updatesDataSource = self . makeUpdatesDataSource ( )
private lazy var installedAppsDataSource = self . makeInstalledAppsDataSource ( )
2019-05-20 21:26:01 +02:00
2019-07-19 16:42:40 -07:00
private var prototypeUpdateCell : UpdateCollectionViewCell !
2019-07-28 15:51:36 -07:00
private var longPressGestureRecognizer : UILongPressGestureRecognizer !
private var sideloadingProgressView : UIProgressView !
2019-05-20 21:26:01 +02:00
2019-07-19 16:42:40 -07:00
// S t a t e
private var isUpdateSectionCollapsed = true
private var expandedAppUpdates = Set < String > ( )
private var isRefreshingAllApps = false
2019-06-21 11:20:03 -07:00
private var refreshGroup : OperationGroup ?
2019-07-28 15:51:36 -07:00
private var sideloadingProgress : Progress ?
2019-06-21 11:20:03 -07:00
2019-07-19 16:42:40 -07:00
// C a c h e
private var cachedUpdateSizes = [ String : CGSize ] ( )
2019-06-10 15:03:47 -07:00
2019-07-19 16:42:40 -07:00
private lazy var dateFormatter : DateFormatter = {
let dateFormatter = DateFormatter ( )
dateFormatter . dateStyle = . medium
dateFormatter . timeStyle = . none
return dateFormatter
} ( )
2019-06-06 12:56:13 -07:00
2019-07-19 16:42:40 -07:00
required init ? ( coder aDecoder : NSCoder )
2019-06-06 12:56:13 -07:00
{
2019-07-19 16:42:40 -07:00
super . init ( coder : aDecoder )
2019-06-06 12:56:13 -07:00
2019-07-30 17:00:04 -07:00
NotificationCenter . default . addObserver ( self , selector : #selector ( MyAppsViewController . didFetchSource ( _ : ) ) , name : AppManager . didFetchSourceNotification , object : nil )
2019-05-20 21:26:01 +02:00
}
2019-05-20 21:40:04 +02:00
2019-07-19 16:42:40 -07:00
override func viewDidLoad ( )
2019-05-20 21:40:04 +02:00
{
2019-07-19 16:42:40 -07:00
super . viewDidLoad ( )
2019-05-20 21:40:04 +02:00
2019-07-24 15:01:33 -07:00
// A l l o w s u s t o i n t e r c e p t d e l e g a t e c a l l b a c k s .
self . updatesDataSource . fetchedResultsController . delegate = self
2019-07-19 16:42:40 -07:00
self . collectionView . dataSource = self . dataSource
self . collectionView . prefetchDataSource = self . dataSource
self . prototypeUpdateCell = UpdateCollectionViewCell . instantiate ( with : UpdateCollectionViewCell . nib ! )
self . prototypeUpdateCell . translatesAutoresizingMaskIntoConstraints = false
self . prototypeUpdateCell . contentView . translatesAutoresizingMaskIntoConstraints = false
2019-05-20 21:40:04 +02:00
2019-07-19 16:42:40 -07:00
self . collectionView . register ( UpdateCollectionViewCell . nib , forCellWithReuseIdentifier : " UpdateCell " )
self . collectionView . register ( UpdatesCollectionHeaderView . self , forSupplementaryViewOfKind : UICollectionView . elementKindSectionHeader , withReuseIdentifier : " UpdatesHeader " )
2019-07-28 15:51:36 -07:00
self . sideloadingProgressView = UIProgressView ( progressViewStyle : . bar )
self . sideloadingProgressView . translatesAutoresizingMaskIntoConstraints = false
2019-09-08 14:21:40 -07:00
self . sideloadingProgressView . progressTintColor = . altRed
2019-07-28 15:51:36 -07:00
self . sideloadingProgressView . progress = 0
if let navigationBar = self . navigationController ? . navigationBar
{
navigationBar . addSubview ( self . sideloadingProgressView )
NSLayoutConstraint . activate ( [ self . sideloadingProgressView . leadingAnchor . constraint ( equalTo : navigationBar . leadingAnchor ) ,
self . sideloadingProgressView . trailingAnchor . constraint ( equalTo : navigationBar . trailingAnchor ) ,
self . sideloadingProgressView . bottomAnchor . constraint ( equalTo : navigationBar . bottomAnchor ) ] )
}
// G e s t u r e s
self . longPressGestureRecognizer = UILongPressGestureRecognizer ( target : self , action : #selector ( MyAppsViewController . handleLongPressGesture ( _ : ) ) )
self . collectionView . addGestureRecognizer ( self . longPressGestureRecognizer )
2019-05-20 21:40:04 +02:00
}
2019-07-24 12:23:54 -07:00
2019-08-28 11:13:22 -07:00
override func viewWillAppear ( _ animated : Bool )
{
super . viewWillAppear ( animated )
self . updateDataSource ( )
}
2019-07-24 12:23:54 -07:00
override func prepare ( for segue : UIStoryboardSegue , sender : Any ? )
{
guard segue . identifier = = " showApp " else { return }
guard let cell = sender as ? UICollectionViewCell , let indexPath = self . collectionView . indexPath ( for : cell ) else { return }
let installedApp = self . dataSource . item ( at : indexPath )
let appViewController = segue . destination as ! AppViewController
2019-07-28 15:08:13 -07:00
appViewController . app = installedApp . storeApp
2019-07-24 12:23:54 -07:00
}
2019-07-28 15:51:36 -07:00
override func shouldPerformSegue ( withIdentifier identifier : String , sender : Any ? ) -> Bool
{
guard identifier = = " showApp " else { return true }
guard let cell = sender as ? UICollectionViewCell , let indexPath = self . collectionView . indexPath ( for : cell ) else { return true }
let installedApp = self . dataSource . item ( at : indexPath )
return ! installedApp . isSideloaded
}
2019-05-20 21:26:01 +02:00
}
private extension MyAppsViewController
{
2019-07-19 16:42:40 -07:00
func makeDataSource ( ) -> RSTCompositeCollectionViewPrefetchingDataSource < InstalledApp , UIImage >
{
2019-07-24 15:01:33 -07:00
let dataSource = RSTCompositeCollectionViewPrefetchingDataSource < InstalledApp , UIImage > ( dataSources : [ self . noUpdatesDataSource , self . updatesDataSource , self . installedAppsDataSource ] )
2019-07-19 16:42:40 -07:00
dataSource . proxy = self
return dataSource
}
2019-07-24 15:01:33 -07:00
func makeNoUpdatesDataSource ( ) -> RSTDynamicCollectionViewPrefetchingDataSource < InstalledApp , UIImage >
{
let dynamicDataSource = RSTDynamicCollectionViewPrefetchingDataSource < InstalledApp , UIImage > ( )
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
2019-09-08 14:21:40 -07:00
cell . contentView . backgroundColor = UIColor . altRed . withAlphaComponent ( 0.15 )
2019-07-24 15:01:33 -07:00
}
return dynamicDataSource
}
2019-07-19 16:42:40 -07:00
func makeUpdatesDataSource ( ) -> RSTFetchedResultsCollectionViewPrefetchingDataSource < InstalledApp , UIImage >
2019-05-20 21:26:01 +02:00
{
2019-07-24 13:52:58 -07:00
let fetchRequest = InstalledApp . updatesFetchRequest ( )
2019-07-28 15:08:13 -07:00
fetchRequest . sortDescriptors = [ NSSortDescriptor ( keyPath : \ InstalledApp . storeApp ? . versionDate , ascending : true ) ,
NSSortDescriptor ( keyPath : \ InstalledApp . name , ascending : true ) ]
2019-05-20 21:26:01 +02:00
fetchRequest . returnsObjectsAsFaults = false
2019-07-19 16:42:40 -07:00
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource < InstalledApp , UIImage > ( fetchRequest : fetchRequest , managedObjectContext : DatabaseManager . shared . viewContext )
dataSource . liveFetchLimit = maximumCollapsedUpdatesCount
dataSource . cellIdentifierHandler = { _ in " UpdateCell " }
dataSource . cellConfigurationHandler = { ( cell , installedApp , indexPath ) in
2019-07-28 15:08:13 -07:00
guard let app = installedApp . storeApp else { return }
2019-05-20 21:26:01 +02:00
2019-07-19 16:42:40 -07:00
let cell = cell as ! UpdateCollectionViewCell
2019-09-08 14:21:40 -07:00
cell . tintColor = app . tintColor ? ? . altRed
2019-07-19 16:42:40 -07:00
cell . nameLabel . text = app . name
cell . versionDescriptionTextView . text = app . versionDescription
2019-08-20 19:06:03 -05:00
cell . appIconImageView . image = nil
cell . appIconImageView . isIndicatingActivity = true
2019-08-28 11:13:22 -07:00
cell . betaBadgeView . isHidden = ! app . isBeta
2019-05-20 21:26:01 +02:00
2019-07-19 16:42:40 -07:00
cell . updateButton . isIndicatingActivity = false
cell . updateButton . addTarget ( self , action : #selector ( MyAppsViewController . updateApp ( _ : ) ) , for : . primaryActionTriggered )
2019-06-06 12:56:13 -07:00
2019-07-28 15:08:13 -07:00
if self . expandedAppUpdates . contains ( app . bundleIdentifier )
2019-06-06 12:56:13 -07:00
{
2019-07-19 16:42:40 -07:00
cell . mode = . expanded
2019-06-06 12:56:13 -07:00
}
else
{
2019-07-19 16:42:40 -07:00
cell . mode = . collapsed
2019-06-06 12:56:13 -07:00
}
2019-07-19 16:42:40 -07:00
2019-07-24 12:23:54 -07:00
cell . versionDescriptionTextView . moreButton . addTarget ( self , action : #selector ( MyAppsViewController . toggleUpdateCellMode ( _ : ) ) , for : . primaryActionTriggered )
2019-07-19 16:42:40 -07:00
let progress = AppManager . shared . installationProgress ( for : app )
cell . updateButton . progress = progress
2019-07-29 16:03:22 -07:00
cell . dateLabel . text = Date ( ) . relativeDateString ( since : app . versionDate , dateFormatter : self . dateFormatter )
2019-07-19 16:42:40 -07:00
cell . setNeedsLayout ( )
2019-05-20 21:26:01 +02:00
}
2019-08-20 19:06:03 -05:00
dataSource . prefetchHandler = { ( installedApp , indexPath , completionHandler ) in
guard let iconURL = installedApp . storeApp ? . iconURL else { return nil }
return RSTAsyncBlockOperation ( ) { ( operation ) in
ImagePipeline . shared . loadImage ( with : iconURL , progress : nil , completion : { ( response , error ) in
2019-08-27 16:00:59 -07:00
guard ! operation . isCancelled else { return operation . finish ( ) }
2019-08-20 19:06:03 -05:00
if let image = response ? . image
{
completionHandler ( image , nil )
}
else
{
completionHandler ( nil , error )
}
} )
}
}
dataSource . prefetchCompletionHandler = { ( cell , image , indexPath , error ) in
let cell = cell as ! UpdateCollectionViewCell
cell . appIconImageView . isIndicatingActivity = false
cell . appIconImageView . image = image
if let error = error
{
print ( " Error loading image: " , error )
}
}
2019-05-20 21:26:01 +02:00
return dataSource
}
2019-06-06 12:56:13 -07:00
2019-07-19 16:42:40 -07:00
func makeInstalledAppsDataSource ( ) -> RSTFetchedResultsCollectionViewPrefetchingDataSource < InstalledApp , UIImage >
2019-06-06 12:56:13 -07:00
{
2019-07-19 16:42:40 -07:00
let fetchRequest = InstalledApp . fetchRequest ( ) as NSFetchRequest < InstalledApp >
2019-07-28 15:08:13 -07:00
fetchRequest . relationshipKeyPathsForPrefetching = [ # keyPath ( InstalledApp . storeApp ) ]
2019-07-19 16:42:40 -07:00
fetchRequest . sortDescriptors = [ NSSortDescriptor ( keyPath : \ InstalledApp . expirationDate , ascending : true ) ,
NSSortDescriptor ( keyPath : \ InstalledApp . refreshedDate , ascending : false ) ,
2019-07-28 15:08:13 -07:00
NSSortDescriptor ( keyPath : \ InstalledApp . name , ascending : true ) ]
2019-07-19 16:42:40 -07:00
fetchRequest . returnsObjectsAsFaults = false
2019-06-06 12:56:13 -07:00
2019-07-19 16:42:40 -07:00
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource < InstalledApp , UIImage > ( fetchRequest : fetchRequest , managedObjectContext : DatabaseManager . shared . viewContext )
dataSource . cellIdentifierHandler = { _ in " AppCell " }
dataSource . cellConfigurationHandler = { ( cell , installedApp , indexPath ) in
2019-09-08 14:21:40 -07:00
let tintColor = installedApp . storeApp ? . tintColor ? ? . altRed
2019-07-19 16:42:40 -07:00
let cell = cell as ! InstalledAppCollectionViewCell
cell . tintColor = tintColor
2019-07-28 15:51:36 -07:00
cell . appIconImageView . isIndicatingActivity = true
2019-08-28 11:13:22 -07:00
cell . betaBadgeView . isHidden = ! ( installedApp . storeApp ? . isBeta ? ? false )
2019-07-28 15:51:36 -07:00
2019-07-19 16:42:40 -07:00
cell . refreshButton . isIndicatingActivity = false
cell . refreshButton . addTarget ( self , action : #selector ( MyAppsViewController . refreshApp ( _ : ) ) , for : . primaryActionTriggered )
let currentDate = Date ( )
let numberOfDays = installedApp . expirationDate . numberOfCalendarDays ( since : currentDate )
if numberOfDays = = 1
{
cell . refreshButton . setTitle ( NSLocalizedString ( " 1 DAY " , comment : " " ) , for : . normal )
}
else
{
cell . refreshButton . setTitle ( String ( format : NSLocalizedString ( " %@ DAYS " , comment : " " ) , NSNumber ( value : numberOfDays ) ) , for : . normal )
}
2019-07-28 15:51:36 -07:00
cell . nameLabel . text = installedApp . name
cell . developerLabel . text = installedApp . storeApp ? . developerName ? ? NSLocalizedString ( " Sideloaded " , comment : " " )
2019-07-19 16:42:40 -07:00
// M a k e s u r e r e f r e s h b u t t o n i s c o r r e c t s i z e .
cell . layoutIfNeeded ( )
switch numberOfDays
{
case 2. . . 3 : cell . refreshButton . tintColor = . refreshOrange
case 4. . . 5 : cell . refreshButton . tintColor = . refreshYellow
case 6. . . : cell . refreshButton . tintColor = . refreshGreen
default : cell . refreshButton . tintColor = . refreshRed
}
2019-07-28 15:51:36 -07:00
if let refreshGroup = self . refreshGroup , let progress = refreshGroup . progress ( for : installedApp ) , progress . fractionCompleted < 1.0
2019-07-19 16:42:40 -07:00
{
cell . refreshButton . progress = progress
}
else
{
cell . refreshButton . progress = nil
}
}
2019-07-28 15:51:36 -07:00
dataSource . prefetchHandler = { ( item , indexPath , completion ) in
let fileURL = item . fileURL
return BlockOperation {
guard let application = ALTApplication ( fileURL : fileURL ) else {
completion ( nil , OperationError . invalidApp )
return
}
let icon = application . icon
completion ( icon , nil )
}
}
dataSource . prefetchCompletionHandler = { ( cell , image , indexPath , error ) in
let cell = cell as ! InstalledAppCollectionViewCell
cell . appIconImageView . image = image
cell . appIconImageView . isIndicatingActivity = false
}
2019-07-19 16:42:40 -07:00
return dataSource
2019-06-06 12:56:13 -07:00
}
2019-08-28 11:13:22 -07:00
func updateDataSource ( )
{
if let patreonAccount = DatabaseManager . shared . patreonAccount ( ) , patreonAccount . isPatron
{
self . dataSource . predicate = nil
}
else
{
2019-09-10 12:32:48 -07:00
self . dataSource . predicate = NSPredicate ( format : " %K == nil OR %K == NO " , # keyPath ( InstalledApp . storeApp ) , # keyPath ( InstalledApp . storeApp . isBeta ) )
2019-08-28 11:13:22 -07:00
}
}
2019-06-06 12:56:13 -07:00
}
private extension MyAppsViewController
{
2019-07-19 16:42:40 -07:00
func update ( )
2019-06-06 12:56:13 -07:00
{
2019-07-19 16:42:40 -07:00
if self . updatesDataSource . itemCount > 0
{
self . navigationController ? . tabBarItem . badgeValue = String ( describing : self . updatesDataSource . itemCount )
2019-07-24 13:52:58 -07:00
UIApplication . shared . applicationIconBadgeNumber = Int ( self . updatesDataSource . itemCount )
2019-07-19 16:42:40 -07:00
}
else
{
self . navigationController ? . tabBarItem . badgeValue = nil
2019-07-24 13:52:58 -07:00
UIApplication . shared . applicationIconBadgeNumber = 0
2019-06-21 11:20:03 -07:00
}
2019-07-24 15:01:33 -07:00
2019-09-12 12:49:19 -07:00
if self . isViewLoaded
{
UIView . performWithoutAnimation {
self . collectionView . reloadSections ( IndexSet ( integer : Section . updates . rawValue ) )
}
}
2019-06-21 11:20:03 -07:00
}
func refresh ( _ installedApps : [ InstalledApp ] , completionHandler : @ escaping ( Result < [ String : Result < InstalledApp , Error > ] , Error > ) -> Void )
{
2019-06-25 14:35:00 -07:00
func refresh ( )
2019-06-21 11:20:03 -07:00
{
2019-06-25 14:35:00 -07:00
let group = AppManager . shared . refresh ( installedApps , presentingViewController : self , group : self . refreshGroup )
group . completionHandler = { ( result ) in
DispatchQueue . main . async {
switch result
2019-06-06 12:56:13 -07:00
{
2019-06-25 14:35:00 -07:00
case . failure ( let error ) :
2019-07-19 16:42:40 -07:00
let toastView = ToastView ( text : error . localizedDescription , detailText : nil )
toastView . setNeedsLayout ( )
2019-06-06 12:56:13 -07:00
toastView . show ( in : self . navigationController ? . view ? ? self . view , duration : 2.0 )
2019-06-25 14:35:00 -07:00
case . success ( let results ) :
2019-07-19 16:42:40 -07:00
let failures = results . compactMapValues { ( result ) -> Error ? in
switch result
{
case . failure ( OperationError . cancelled ) : return nil
case . failure ( let error ) : return error
case . success : return nil
}
}
guard ! failures . isEmpty else { break }
2019-06-25 14:35:00 -07:00
2019-07-19 16:42:40 -07:00
let localizedText : String
2019-09-12 12:59:56 -07:00
let detailText : String ?
2019-07-19 16:42:40 -07:00
if let failure = failures . first , failures . count = = 1
2019-06-06 12:56:13 -07:00
{
2019-07-19 16:42:40 -07:00
localizedText = failure . value . localizedDescription
2019-09-12 12:59:56 -07:00
detailText = nil
2019-06-06 12:56:13 -07:00
}
else
{
2019-07-19 16:42:40 -07:00
localizedText = String ( format : NSLocalizedString ( " Failed to refresh %@ apps. " , comment : " " ) , NSNumber ( value : failures . count ) )
2019-09-12 12:59:56 -07:00
detailText = failures . first ? . value . localizedDescription
2019-06-06 12:56:13 -07:00
}
2019-09-12 12:59:56 -07:00
let toastView = ToastView ( text : localizedText , detailText : detailText )
2019-07-19 16:42:40 -07:00
toastView . show ( in : self . navigationController ? . view ? ? self . view , duration : 2.0 )
2019-06-06 12:56:13 -07:00
}
2019-06-25 14:35:00 -07:00
self . refreshGroup = nil
completionHandler ( result )
2019-06-06 12:56:13 -07:00
}
2019-06-10 15:03:47 -07:00
}
2019-06-25 14:35:00 -07:00
self . refreshGroup = group
2019-07-19 16:42:40 -07:00
2019-07-24 15:01:33 -07:00
UIView . performWithoutAnimation {
self . collectionView . reloadSections ( IndexSet ( integer : Section . installedApps . rawValue ) )
}
2019-06-10 15:03:47 -07:00
}
2019-07-31 14:07:00 -07:00
if installedApps . contains ( where : { $0 . bundleIdentifier = = StoreApp . altstoreAppID } )
2019-06-25 14:35:00 -07:00
{
let alertController = UIAlertController ( title : NSLocalizedString ( " Refresh AltStore? " , comment : " " ) , message : NSLocalizedString ( " AltStore will quit when it is finished refreshing. " , comment : " " ) , preferredStyle : . alert )
2019-07-19 16:42:40 -07:00
alertController . addAction ( UIAlertAction ( title : RSTSystemLocalizedString ( " Cancel " ) , style : . cancel ) { ( action ) in
completionHandler ( . failure ( OperationError . cancelled ) )
} )
2019-06-25 14:35:00 -07:00
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Refresh " , comment : " " ) , style : . default ) { ( action ) in
refresh ( )
} )
self . present ( alertController , animated : true , completion : nil )
}
else
{
refresh ( )
}
2019-06-06 12:56:13 -07:00
}
2019-05-20 21:26:01 +02:00
}
2019-05-20 21:36:39 +02:00
2019-07-19 16:42:40 -07:00
private extension MyAppsViewController
2019-05-20 21:36:39 +02:00
{
2019-07-19 16:42:40 -07:00
@IBAction func toggleAppUpdates ( _ sender : UIButton )
2019-05-20 21:36:39 +02:00
{
2019-07-19 16:42:40 -07:00
let visibleCells = self . collectionView . visibleCells
self . collectionView . performBatchUpdates ( {
2019-05-20 21:36:39 +02:00
2019-07-19 16:42:40 -07:00
self . isUpdateSectionCollapsed . toggle ( )
UIView . animate ( withDuration : 0.3 , animations : {
if self . isUpdateSectionCollapsed
2019-06-05 11:03:49 -07:00
{
2019-07-19 16:42:40 -07:00
self . updatesDataSource . liveFetchLimit = maximumCollapsedUpdatesCount
self . expandedAppUpdates . removeAll ( )
for case let cell as UpdateCollectionViewCell in visibleCells
{
cell . mode = . collapsed
}
self . cachedUpdateSizes . removeAll ( )
sender . titleLabel ? . transform = . identity
2019-06-05 11:03:49 -07:00
}
2019-07-19 16:42:40 -07:00
else
2019-06-05 11:03:49 -07:00
{
2019-07-19 16:42:40 -07:00
self . updatesDataSource . liveFetchLimit = 0
sender . titleLabel ? . transform = CGAffineTransform . identity . rotated ( by : . pi )
2019-06-05 11:03:49 -07:00
}
2019-07-19 16:42:40 -07:00
} )
self . collectionView . collectionViewLayout . invalidateLayout ( )
} , completion : nil )
}
@IBAction func toggleUpdateCellMode ( _ sender : UIButton )
{
let point = self . collectionView . convert ( sender . center , from : sender . superview )
guard let indexPath = self . collectionView . indexPathForItem ( at : point ) else { return }
let installedApp = self . dataSource . item ( at : indexPath )
let cell = self . collectionView . cellForItem ( at : indexPath ) as ? UpdateCollectionViewCell
2019-07-28 15:08:13 -07:00
if self . expandedAppUpdates . contains ( installedApp . bundleIdentifier )
2019-07-19 16:42:40 -07:00
{
2019-07-28 15:08:13 -07:00
self . expandedAppUpdates . remove ( installedApp . bundleIdentifier )
2019-07-19 16:42:40 -07:00
cell ? . mode = . collapsed
}
else
{
2019-07-28 15:08:13 -07:00
self . expandedAppUpdates . insert ( installedApp . bundleIdentifier )
2019-07-19 16:42:40 -07:00
cell ? . mode = . expanded
2019-06-05 11:03:49 -07:00
}
2019-07-28 15:08:13 -07:00
self . cachedUpdateSizes [ installedApp . bundleIdentifier ] = nil
2019-07-19 16:42:40 -07:00
self . collectionView . performBatchUpdates ( {
self . collectionView . collectionViewLayout . invalidateLayout ( )
} , completion : nil )
}
@IBAction func refreshApp ( _ sender : UIButton )
{
let point = self . collectionView . convert ( sender . center , from : sender . superview )
guard let indexPath = self . collectionView . indexPathForItem ( at : point ) else { return }
let installedApp = self . dataSource . item ( at : indexPath )
2019-07-28 15:08:13 -07:00
let previousProgress = AppManager . shared . refreshProgress ( for : installedApp )
2019-07-19 16:42:40 -07:00
guard previousProgress = = nil else {
previousProgress ? . cancel ( )
return
2019-05-20 21:36:39 +02:00
}
2019-06-05 11:03:49 -07:00
2019-07-19 16:42:40 -07:00
self . refresh ( [ installedApp ] ) { ( result ) in
2019-07-31 13:54:54 -07:00
// I f a n e r r o r o c c u r e d , r e l o a d t h e s e c t i o n s o t h e p r o g r e s s b a r i s n o l o n g e r v i s i b l e .
if result . error != nil || result . value ? . values . contains ( where : { $0 . error != nil } ) = = true
{
DispatchQueue . main . async {
self . collectionView . reloadSections ( IndexSet ( integer : Section . installedApps . rawValue ) )
}
}
2019-07-24 15:01:33 -07:00
print ( " Finished refreshing with result: " , result . error ? . localizedDescription ? ? " success " )
2019-07-19 16:42:40 -07:00
}
2019-06-05 11:03:49 -07:00
}
2019-07-19 16:42:40 -07:00
@IBAction func refreshAllApps ( _ sender : UIBarButtonItem )
2019-06-05 11:03:49 -07:00
{
2019-07-19 16:42:40 -07:00
self . isRefreshingAllApps = true
self . collectionView . collectionViewLayout . invalidateLayout ( )
let installedApps = InstalledApp . fetchAppsForRefreshingAll ( in : DatabaseManager . shared . viewContext )
self . refresh ( installedApps ) { ( result ) in
DispatchQueue . main . async {
self . isRefreshingAllApps = false
self . collectionView . reloadSections ( IndexSet ( integer : Section . installedApps . rawValue ) )
}
}
2019-05-20 21:36:39 +02:00
}
2019-06-06 12:56:13 -07:00
2019-07-19 16:42:40 -07:00
@IBAction func updateApp ( _ sender : UIButton )
2019-06-06 12:56:13 -07:00
{
2019-07-19 16:42:40 -07:00
let point = self . collectionView . convert ( sender . center , from : sender . superview )
guard let indexPath = self . collectionView . indexPathForItem ( at : point ) else { return }
2019-06-06 12:56:13 -07:00
2019-07-28 15:08:13 -07:00
guard let storeApp = self . dataSource . item ( at : indexPath ) . storeApp else { return }
2019-06-06 12:56:13 -07:00
2019-07-28 15:08:13 -07:00
let previousProgress = AppManager . shared . installationProgress ( for : storeApp )
2019-07-19 16:42:40 -07:00
guard previousProgress = = nil else {
previousProgress ? . cancel ( )
return
}
2019-07-28 15:08:13 -07:00
_ = AppManager . shared . install ( storeApp , presentingViewController : self ) { ( result ) in
2019-07-19 16:42:40 -07:00
DispatchQueue . main . async {
switch result
{
case . failure ( OperationError . cancelled ) :
self . collectionView . reloadItems ( at : [ indexPath ] )
case . failure ( let error ) :
2019-07-24 12:51:23 -07:00
let toastView = ToastView ( text : error . localizedDescription , detailText : nil )
toastView . show ( in : self . navigationController ? . view ? ? self . view , duration : 2 )
2019-07-19 16:42:40 -07:00
self . collectionView . reloadItems ( at : [ indexPath ] )
case . success :
2019-07-28 15:08:13 -07:00
print ( " Updated app: " , storeApp . bundleIdentifier )
2019-07-19 16:42:40 -07:00
// N o n e e d t o r e l o a d , s i n c e t h e t h e u p d a t e c e l l i s g o n e n o w .
}
2019-07-24 15:01:33 -07:00
self . update ( )
2019-07-19 16:42:40 -07:00
}
}
self . collectionView . reloadItems ( at : [ indexPath ] )
}
2019-07-28 15:51:36 -07:00
@IBAction func sideloadApp ( _ sender : UIBarButtonItem )
{
2019-07-30 17:18:51 -07:00
func sideloadApp ( )
{
let iOSAppUTI = " com.apple.itunes.ipa " // D e c l a r e d b y t h e s y s t e m .
let documentPickerViewController = UIDocumentPickerViewController ( documentTypes : [ iOSAppUTI ] , in : . import )
documentPickerViewController . delegate = self
self . present ( documentPickerViewController , animated : true , completion : nil )
}
2019-07-28 15:51:36 -07:00
2019-07-30 17:18:51 -07:00
let alertController = UIAlertController ( title : NSLocalizedString ( " Sideload Apps (Beta) " , comment : " " ) , message : NSLocalizedString ( " You may only install 10 apps + app extensions per week due to Apple's restrictions. \n \n If you encounter an app that is not able to be sideloaded, please report the app to riley@rileytestut.com. " , comment : " " ) , preferredStyle : . alert )
alertController . addAction ( UIAlertAction ( title : RSTSystemLocalizedString ( " OK " ) , style : . default , handler : { ( action ) in
sideloadApp ( )
} ) )
alertController . addAction ( . cancel )
self . present ( alertController , animated : true , completion : nil )
2019-07-28 15:51:36 -07:00
}
@objc func presentAlert ( for installedApp : InstalledApp )
{
let alertController = UIAlertController ( title : nil , message : NSLocalizedString ( " Removing a sideloaded app only removes it from AltStore. You must also delete it from the home screen to fully uninstall the app. " , comment : " " ) , preferredStyle : . actionSheet )
alertController . addAction ( . cancel )
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Remove " , comment : " " ) , style : . destructive , handler : { ( action ) in
DatabaseManager . shared . persistentContainer . performBackgroundTask { ( context ) in
let installedApp = context . object ( with : installedApp . objectID ) as ! InstalledApp
context . delete ( installedApp )
do { try context . save ( ) }
catch { print ( " Failed to remove sideloaded app. " , error ) }
}
} ) )
self . present ( alertController , animated : true , completion : nil )
}
}
private extension MyAppsViewController
{
2019-07-30 17:00:04 -07:00
@objc func didFetchSource ( _ notification : Notification )
2019-07-19 16:42:40 -07:00
{
DispatchQueue . main . async {
if self . updatesDataSource . fetchedResultsController . fetchedObjects = = nil
{
do { try self . updatesDataSource . fetchedResultsController . performFetch ( ) }
catch { print ( " Error fetching: " , error ) }
}
self . update ( )
}
}
2019-07-28 15:51:36 -07:00
@objc func handleLongPressGesture ( _ gestureRecognizer : UILongPressGestureRecognizer )
{
guard gestureRecognizer . state = = . began else { return }
let point = gestureRecognizer . location ( in : self . collectionView )
guard
let indexPath = self . collectionView . indexPathForItem ( at : point ) ,
indexPath . section = = Section . installedApps . rawValue
else { return }
let installedApp = self . dataSource . item ( at : indexPath )
guard installedApp . storeApp = = nil else { return }
self . presentAlert ( for : installedApp )
}
2019-07-19 16:42:40 -07:00
}
extension MyAppsViewController
{
override func collectionView ( _ collectionView : UICollectionView , viewForSupplementaryElementOfKind kind : String , at indexPath : IndexPath ) -> UICollectionReusableView
{
2019-07-31 13:37:51 -07:00
let section = Section ( rawValue : indexPath . section ) !
switch section
2019-07-19 16:42:40 -07:00
{
2019-07-31 13:37:51 -07:00
case . noUpdates : return UICollectionReusableView ( )
case . updates :
2019-07-19 16:42:40 -07:00
let headerView = collectionView . dequeueReusableSupplementaryView ( ofKind : UICollectionView . elementKindSectionHeader , withReuseIdentifier : " UpdatesHeader " , for : indexPath ) as ! UpdatesCollectionHeaderView
UIView . performWithoutAnimation {
2019-09-08 14:21:40 -07:00
headerView . button . backgroundColor = UIColor . altRed . withAlphaComponent ( 0.15 )
2019-07-19 16:42:40 -07:00
headerView . button . setTitle ( " ▾ " , for : . normal )
headerView . button . titleLabel ? . font = UIFont . boldSystemFont ( ofSize : 28 )
2019-09-08 14:21:40 -07:00
headerView . button . setTitleColor ( . altRed , for : . normal )
2019-07-19 16:42:40 -07:00
headerView . button . addTarget ( self , action : #selector ( MyAppsViewController . toggleAppUpdates ) , for : . primaryActionTriggered )
2019-07-24 15:01:33 -07:00
if self . isUpdateSectionCollapsed
{
headerView . button . titleLabel ? . transform = . identity
}
else
{
headerView . button . titleLabel ? . transform = CGAffineTransform . identity . rotated ( by : . pi )
}
headerView . isHidden = ( self . updatesDataSource . itemCount <= 2 )
2019-07-19 16:42:40 -07:00
headerView . button . layoutIfNeeded ( )
}
return headerView
2019-07-31 13:37:51 -07:00
case . installedApps :
2019-07-19 16:42:40 -07:00
let headerView = collectionView . dequeueReusableSupplementaryView ( ofKind : UICollectionView . elementKindSectionHeader , withReuseIdentifier : " InstalledAppsHeader " , for : indexPath ) as ! InstalledAppsCollectionHeaderView
UIView . performWithoutAnimation {
headerView . textLabel . text = NSLocalizedString ( " Installed " , comment : " " )
headerView . button . isIndicatingActivity = false
2019-09-08 14:21:40 -07:00
headerView . button . activityIndicatorView . color = . altRed
2019-07-19 16:42:40 -07:00
headerView . button . setTitle ( NSLocalizedString ( " Refresh All " , comment : " " ) , for : . normal )
headerView . button . addTarget ( self , action : #selector ( MyAppsViewController . refreshAllApps ( _ : ) ) , for : . primaryActionTriggered )
headerView . button . isIndicatingActivity = self . isRefreshingAllApps
headerView . button . layoutIfNeeded ( )
}
return headerView
}
}
}
extension MyAppsViewController : UICollectionViewDelegateFlowLayout
{
func collectionView ( _ collectionView : UICollectionView , layout collectionViewLayout : UICollectionViewLayout , sizeForItemAt indexPath : IndexPath ) -> CGSize
{
2019-07-24 15:01:33 -07:00
let padding = 30 as CGFloat
let width = collectionView . bounds . width - padding
2019-07-19 16:42:40 -07:00
let section = Section . allCases [ indexPath . section ]
switch section
{
2019-07-24 15:01:33 -07:00
case . noUpdates :
let size = CGSize ( width : width , height : 44 )
return size
2019-07-19 16:42:40 -07:00
case . updates :
let item = self . dataSource . item ( at : indexPath )
2019-07-28 15:08:13 -07:00
if let previousHeight = self . cachedUpdateSizes [ item . bundleIdentifier ]
2019-07-19 16:42:40 -07:00
{
return previousHeight
}
let widthConstraint = self . prototypeUpdateCell . contentView . widthAnchor . constraint ( equalToConstant : width )
NSLayoutConstraint . activate ( [ widthConstraint ] )
defer { NSLayoutConstraint . deactivate ( [ widthConstraint ] ) }
self . dataSource . cellConfigurationHandler ( self . prototypeUpdateCell , item , indexPath )
let size = self . prototypeUpdateCell . contentView . systemLayoutSizeFitting ( UIView . layoutFittingCompressedSize )
2019-07-28 15:08:13 -07:00
self . cachedUpdateSizes [ item . bundleIdentifier ] = size
2019-07-19 16:42:40 -07:00
return size
case . installedApps :
return CGSize ( width : collectionView . bounds . width , height : 60 )
}
}
func collectionView ( _ collectionView : UICollectionView , layout collectionViewLayout : UICollectionViewLayout , referenceSizeForHeaderInSection section : Int ) -> CGSize
{
let section = Section . allCases [ section ]
switch section
{
2019-07-24 15:01:33 -07:00
case . noUpdates : return . zero
case . updates :
let height : CGFloat = self . updatesDataSource . itemCount > maximumCollapsedUpdatesCount ? 26 : 0
return CGSize ( width : collectionView . bounds . width , height : height )
2019-07-19 16:42:40 -07:00
case . installedApps : return CGSize ( width : collectionView . bounds . width , height : 29 )
}
}
func collectionView ( _ collectionView : UICollectionView , layout collectionViewLayout : UICollectionViewLayout , insetForSectionAt section : Int ) -> UIEdgeInsets
{
let section = Section . allCases [ section ]
switch section
{
2019-07-24 15:01:33 -07:00
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 < NSFetchRequestResult > )
{
2019-07-31 11:24:18 -07:00
// R e s p o n d i n g t o N S F e t c h e d R e s u l t s C o n t r o l l e r u p d a t e s b e f o r e t h e c o l l e c t i o n v i e w h a s
// b e e n s h o w n m a y t h r o w e x c e p t i o n s b e c a u s e t h e c o l l e c t i o n v i e w c a n n o t a c c u r a t e l y
// c o u n t t h e n u m b e r o f i t e m s b e f o r e t h e u p d a t e . H o w e v e r , i f w e m a n u a l l y c a l l
// p e r f o r m B a t c h U p d a t e s _ b e f o r e _ r e s p o n d i n g t o u p d a t e s , t h e c o l l e c t i o n v i e w c a n g e t
// a n a c c u r a t e p r e - u p d a t e i t e m c o u n t .
self . collectionView . performBatchUpdates ( nil , completion : nil )
2019-07-24 15:01:33 -07:00
self . updatesDataSource . controllerWillChangeContent ( controller )
}
func controller ( _ controller : NSFetchedResultsController < NSFetchRequestResult > , didChange sectionInfo : NSFetchedResultsSectionInfo , atSectionIndex sectionIndex : Int , for type : NSFetchedResultsChangeType )
{
self . updatesDataSource . controller ( controller , didChange : sectionInfo , atSectionIndex : UInt ( sectionIndex ) , for : type )
}
func controller ( _ controller : NSFetchedResultsController < NSFetchRequestResult > , 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 < NSFetchRequestResult > )
{
let previousUpdateCount = self . collectionView . numberOfItems ( inSection : Section . updates . rawValue )
let updateCount = Int ( self . updatesDataSource . itemCount )
if previousUpdateCount = = 0 && updateCount > 0
{
// R e m o v e " N o U p d a t e s A v a i l a b l e " c e l l .
let change = RSTCellContentChange ( type : . delete , currentIndexPath : IndexPath ( item : 0 , section : Section . noUpdates . rawValue ) , destinationIndexPath : nil )
self . collectionView . add ( change )
2019-07-19 16:42:40 -07:00
}
2019-07-24 15:01:33 -07:00
else if previousUpdateCount > 0 && updateCount = = 0
{
// I n s e r t " N o U p d a t e s A v a i l a b l e " c e l l .
let change = RSTCellContentChange ( type : . insert , currentIndexPath : nil , destinationIndexPath : IndexPath ( item : 0 , section : Section . noUpdates . rawValue ) )
self . collectionView . add ( change )
}
self . updatesDataSource . controllerDidChangeContent ( controller )
2019-06-06 12:56:13 -07:00
}
2019-05-20 21:36:39 +02:00
}
2019-07-28 15:51:36 -07:00
extension MyAppsViewController : UIDocumentPickerDelegate
{
func documentPicker ( _ controller : UIDocumentPickerViewController , didPickDocumentsAt urls : [ URL ] )
{
guard let fileURL = urls . first else { return }
self . navigationItem . leftBarButtonItem ? . isIndicatingActivity = true
DispatchQueue . global ( ) . async {
let temporaryDirectory = FileManager . default . uniqueTemporaryURL ( )
do
{
try FileManager . default . createDirectory ( at : temporaryDirectory , withIntermediateDirectories : true , attributes : nil )
let unzippedApplicationURL = try FileManager . default . unzipAppBundle ( at : fileURL , toDirectory : temporaryDirectory )
guard let application = ALTApplication ( fileURL : unzippedApplicationURL ) else { return }
self . sideloadingProgress = AppManager . shared . install ( application , presentingViewController : self ) { ( result ) in
try ? FileManager . default . removeItem ( at : temporaryDirectory )
DispatchQueue . main . async {
if let error = result . error
{
let toastView = ToastView ( text : error . localizedDescription , detailText : nil )
toastView . show ( in : self . navigationController ? . view ? ? self . view , duration : 2.0 )
}
else
{
print ( " Successfully installed app: " , application . bundleIdentifier )
}
self . navigationItem . leftBarButtonItem ? . isIndicatingActivity = false
self . sideloadingProgressView . observedProgress = nil
self . sideloadingProgressView . setHidden ( true , animated : true )
}
}
DispatchQueue . main . async {
self . sideloadingProgressView . progress = 0
self . sideloadingProgressView . isHidden = false
self . sideloadingProgressView . observedProgress = self . sideloadingProgress
}
}
catch
{
try ? FileManager . default . removeItem ( at : temporaryDirectory )
self . navigationItem . leftBarButtonItem ? . isIndicatingActivity = false
}
}
}
}