[AltStore] Adds redesigned BrowseViewController to browse and install apps

This commit is contained in:
Riley Testut
2019-07-16 14:25:09 -07:00
parent 800ec11ae1
commit 129ae15a54
16 changed files with 689 additions and 22 deletions

View File

@@ -0,0 +1,101 @@
//
// BrowseCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
@objc class BrowseCollectionViewCell: UICollectionViewCell
{
var imageNames: [String] = [] {
didSet {
self.dataSource.items = self.imageNames.map { $0 as NSString }
}
}
private lazy var dataSource = self.makeDataSource()
private lazy var imageSizes = [NSString: CGSize]()
@IBOutlet var nameLabel: UILabel!
@IBOutlet var developerLabel: UILabel!
@IBOutlet var appIconImageView: UIImageView!
@IBOutlet var actionButton: ProgressButton!
@IBOutlet var subtitleLabel: UILabel!
@IBOutlet private var screenshotsContentView: UIView!
@IBOutlet private var screenshotsCollectionView: UICollectionView!
override func awakeFromNib()
{
super.awakeFromNib()
self.screenshotsCollectionView.delegate = self
self.screenshotsCollectionView.dataSource = self.dataSource
self.screenshotsCollectionView.prefetchDataSource = self.dataSource
self.screenshotsContentView.layer.cornerRadius = 20
self.screenshotsContentView.layer.masksToBounds = true
self.update()
}
override func tintColorDidChange()
{
super.tintColorDidChange()
self.update()
}
}
private extension BrowseCollectionViewCell
{
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSString, UIImage>
{
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSString, UIImage>(items: [])
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = true
}
dataSource.prefetchHandler = { (imageName, indexPath, completion) in
return BlockOperation {
let image = UIImage(named: imageName as String)
completion(image, nil)
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false
cell.imageView.image = image
}
return dataSource
}
private func update()
{
self.subtitleLabel.textColor = self.tintColor
self.screenshotsContentView.backgroundColor = self.tintColor.withAlphaComponent(0.1)
}
}
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let imageURL = self.dataSource.item(at: indexPath)
let dimensions = self.imageSizes[imageURL] ?? UIScreen.main.nativeBounds.size
let aspectRatio = dimensions.width / dimensions.height
let height = self.screenshotsCollectionView.bounds.height
let width = (self.screenshotsCollectionView.bounds.height * aspectRatio).rounded(.down)
let size = CGSize(width: width, height: height)
return size
}
}

View File

@@ -0,0 +1,172 @@
//
// BrowseViewController.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class BrowseViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
override func viewDidLoad()
{
super.viewDidLoad()
self.collectionView.dataSource = self.dataSource
self.collectionView.prefetchDataSource = self.dataSource
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.fetchApps()
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
let collectionViewLayout = self.collectionViewLayout as! UICollectionViewFlowLayout
collectionViewLayout.itemSize.width = self.view.bounds.width
}
}
private extension BrowseViewController
{
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<App, UIImage>
{
let fetchRequest = App.fetchRequest() as NSFetchRequest<App>
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)]
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \App.name, ascending: false)]
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(App.identifier), App.altstoreAppID)
fetchRequest.returnsObjectsAsFaults = false
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<App, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellConfigurationHandler = { [weak self] (cell, app, indexPath) in
guard let `self` = self else { return }
let cell = cell as! BrowseCollectionViewCell
cell.nameLabel.text = app.name
cell.developerLabel.text = app.developerName
cell.subtitleLabel.text = app.subtitle
cell.imageNames = Array(app.screenshotNames.prefix(3))
cell.appIconImageView.image = UIImage(named: app.iconName)
cell.actionButton.tag = indexPath.item
cell.actionButton.activityIndicatorView.style = .white
// Explicitly set to false to ensure we're starting from a non-activity indicating state.
// Otherwise, cell reuse can mess up some cached values.
cell.actionButton.isIndicatingActivity = false
let tintColor = app.tintColor ?? self.collectionView.tintColor!
cell.tintColor = tintColor
cell.actionButton.progressTintColor = tintColor
if app.installedApp == nil
{
cell.actionButton.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
cell.actionButton.setTitleColor(.altGreen, for: .normal)
cell.actionButton.backgroundColor = UIColor.altGreen.withAlphaComponent(0.1)
if let progress = AppManager.shared.installationProgress(for: app)
{
cell.actionButton.progress = progress
cell.actionButton.isIndicatingActivity = true
cell.actionButton.activityIndicatorView.isUserInteractionEnabled = false
cell.actionButton.isUserInteractionEnabled = true
}
else
{
cell.actionButton.progress = nil
cell.actionButton.isIndicatingActivity = false
}
}
else
{
cell.actionButton.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
cell.actionButton.setTitleColor(.white, for: .normal)
cell.actionButton.backgroundColor = .altGreen
}
}
return dataSource
}
func fetchApps()
{
AppManager.shared.fetchApps() { (result) in
do
{
let apps = try result.get()
try apps.first?.managedObjectContext?.save()
}
catch
{
DispatchQueue.main.async {
let toastView = RSTToastView(text: NSLocalizedString("Failed to Fetch Apps", comment: ""), detailText: error.localizedDescription)
toastView.tintColor = .altGreen
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
}
}
}
}
}
private extension BrowseViewController
{
@IBAction func performAppAction(_ sender: ProgressButton)
{
let indexPath = IndexPath(item: sender.tag, section: 0)
let app = self.dataSource.item(at: indexPath)
if let installedApp = app.installedApp
{
self.open(installedApp)
}
else
{
self.install(app, at: indexPath)
}
}
func install(_ app: App, at indexPath: IndexPath)
{
let previousProgress = AppManager.shared.installationProgress(for: app)
guard previousProgress == nil else {
previousProgress?.cancel()
return
}
_ = AppManager.shared.install(app, presentingViewController: self) { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(OperationError.cancelled): break // Ignore
case .failure(let error):
let toastView = RSTToastView(text: "Failed to install \(app.name)", detailText: error.localizedDescription)
toastView.tintColor = .altGreen
toastView.show(in: self.navigationController!.view, duration: 2)
case .success(let installedApp): print("Installed app:", installedApp.app.identifier)
}
self.collectionView.reloadItems(at: [indexPath])
}
}
self.collectionView.reloadItems(at: [indexPath])
}
func open(_ installedApp: InstalledApp)
{
UIApplication.shared.open(installedApp.openAppURL)
}
}

View File

@@ -0,0 +1,28 @@
//
// ScreenshotCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
@objc(ScreenshotCollectionViewCell)
class ScreenshotCollectionViewCell: UICollectionViewCell
{
let imageView: UIImageView
required init?(coder aDecoder: NSCoder)
{
self.imageView = UIImageView(image: nil)
self.imageView.layer.cornerRadius = 8
self.imageView.layer.masksToBounds = true
super.init(coder: aDecoder)
self.addSubview(self.imageView, pinningEdgesWith: .zero)
}
}