// // AppIDsViewController.swift // AltStore // // Created by Riley Testut on 1/27/20. // Copyright © 2020 Riley Testut. All rights reserved. // import UIKit import Roxas class AppIDsViewController: UICollectionViewController { private lazy var dataSource = self.makeDataSource() private var didInitialFetch = false private var isLoading = false { didSet { self.update() } } @IBOutlet var activityIndicatorBarButtonItem: UIBarButtonItem! override func viewDidLoad() { super.viewDidLoad() self.collectionView.dataSource = self.dataSource self.activityIndicatorBarButtonItem.isIndicatingActivity = true let refreshControl = UIRefreshControl() refreshControl.addTarget(self, action: #selector(AppIDsViewController.fetchAppIDs), for: .primaryActionTriggered) self.collectionView.refreshControl = refreshControl } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if !self.didInitialFetch { self.fetchAppIDs() } } } private extension AppIDsViewController { func makeDataSource() -> RSTFetchedResultsCollectionViewDataSource { let fetchRequest = AppID.fetchRequest() as NSFetchRequest fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \AppID.name, ascending: true), NSSortDescriptor(keyPath: \AppID.bundleIdentifier, ascending: true), NSSortDescriptor(keyPath: \AppID.expirationDate, ascending: true)] fetchRequest.returnsObjectsAsFaults = false if let team = DatabaseManager.shared.activeTeam() { fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(AppID.team), team) } else { fetchRequest.predicate = NSPredicate(value: false) } let dataSource = RSTFetchedResultsCollectionViewDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) dataSource.proxy = self dataSource.cellConfigurationHandler = { (cell, appID, indexPath) in let tintColor = UIColor.altPrimary let cell = cell as! AppIDCollectionViewCell cell.layoutMargins.left = self.view.layoutMargins.left cell.layoutMargins.right = self.view.layoutMargins.right cell.tintColor = tintColor cell.bannerView.iconImageView.isHidden = true cell.bannerView.button.isIndicatingActivity = false cell.bannerView.betaBadgeView.isHidden = true if let expirationDate = appID.expirationDate { cell.bannerView.button.isHidden = false cell.bannerView.button.isUserInteractionEnabled = false cell.bannerView.buttonLabel.isHidden = false let currentDate = Date() let numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate) if numberOfDays == 1 { cell.bannerView.button.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal) } else { cell.bannerView.button.setTitle(String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)), for: .normal) } } else { cell.bannerView.button.isHidden = true cell.bannerView.buttonLabel.isHidden = true } cell.bannerView.titleLabel.text = appID.name cell.bannerView.subtitleLabel.text = appID.bundleIdentifier cell.bannerView.subtitleLabel.numberOfLines = 2 // Make sure refresh button is correct size. cell.layoutIfNeeded() } return dataSource } @objc func fetchAppIDs() { guard !self.isLoading else { return } self.isLoading = true AppManager.shared.fetchAppIDs { (result) in do { let (_, context) = try result.get() try context.save() } catch { DispatchQueue.main.async { let toastView = ToastView(error: error) toastView.show(in: self) } } DispatchQueue.main.async { self.isLoading = false } } } func update() { if !self.isLoading { self.collectionView.refreshControl?.endRefreshing() self.activityIndicatorBarButtonItem.isIndicatingActivity = false } } } extension AppIDsViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: collectionView.bounds.width, height: 80) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { let indexPath = IndexPath(row: 0, section: section) let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath) // Use this view to calculate the optimal size based on the collection view's width let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height), withHorizontalFittingPriority: .required, // Width is fixed verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed return size } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { return CGSize(width: collectionView.bounds.width, height: 50) } override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { switch kind { case UICollectionView.elementKindSectionHeader: let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! AppIDsCollectionReusableView headerView.layoutMargins.left = self.view.layoutMargins.left headerView.layoutMargins.right = self.view.layoutMargins.right if let activeTeam = DatabaseManager.shared.activeTeam(), activeTeam.type == .free { headerView.textLabel.text = """ Each app and app extension installed with AltStore must register an App ID with Apple. Apple limits free developer accounts to 10 App IDs at a time. App IDs expire after one week, but AltStore will automatically renew them for all installed apps. Once an App ID expires, it no longer counts toward your total. """ } else { headerView.textLabel.text = """ Each app and app extension installed with AltStore must register an App ID with Apple. App IDs for paid developer accounts never expire, and there is no limit to how many you can create. """ } return headerView case UICollectionView.elementKindSectionFooter: let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! AppIDsCollectionReusableView let count = self.dataSource.itemCount if count == 1 { footerView.textLabel.text = NSLocalizedString("1 App ID", comment: "") } else { footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs", comment: ""), NSNumber(value: count)) } return footerView default: fatalError() } } }