mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 19:53:25 +01:00
Accurately displays dynamically-sized screenshots in AppViewController
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// AppScreenshotCollectionViewCell.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/11/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
class AppScreenshotCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
let imageView: UIImageView
|
||||
|
||||
var aspectRatio: CGSize = AppScreenshot.defaultAspectRatio {
|
||||
didSet {
|
||||
self.updateAspectRatio()
|
||||
}
|
||||
}
|
||||
|
||||
private var isRounded: Bool = false {
|
||||
didSet {
|
||||
self.setNeedsLayout()
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private var aspectRatioConstraint: NSLayoutConstraint?
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
self.imageView = UIImageView(frame: .zero)
|
||||
self.imageView.clipsToBounds = true
|
||||
self.imageView.layer.cornerCurve = .continuous
|
||||
self.imageView.layer.borderColor = UIColor.tertiaryLabel.cgColor
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.contentView.addSubview(self.imageView)
|
||||
|
||||
let widthConstraint = self.imageView.widthAnchor.constraint(equalTo: self.contentView.widthAnchor)
|
||||
widthConstraint.priority = UILayoutPriority(999)
|
||||
|
||||
let heightConstraint = self.imageView.heightAnchor.constraint(equalTo: self.contentView.heightAnchor)
|
||||
heightConstraint.priority = UILayoutPriority(999)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
widthConstraint,
|
||||
heightConstraint,
|
||||
self.imageView.widthAnchor.constraint(lessThanOrEqualTo: self.contentView.widthAnchor),
|
||||
self.imageView.heightAnchor.constraint(lessThanOrEqualTo: self.contentView.heightAnchor),
|
||||
self.imageView.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor),
|
||||
self.imageView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor)
|
||||
])
|
||||
|
||||
self.updateAspectRatio()
|
||||
self.updateTraits()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
|
||||
{
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
self.updateTraits()
|
||||
}
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
guard self.imageView.bounds.width != 0 else {
|
||||
self.setNeedsLayout()
|
||||
return
|
||||
}
|
||||
|
||||
if self.isRounded
|
||||
{
|
||||
let cornerRadius = self.imageView.bounds.width / 9.0 // Based on iPhone 15
|
||||
self.imageView.layer.cornerRadius = cornerRadius
|
||||
}
|
||||
else
|
||||
{
|
||||
let cornerRadius = self.imageView.bounds.width / 25.0 // Based on iPhone 8
|
||||
self.imageView.layer.cornerRadius = cornerRadius
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppScreenshotCollectionViewCell
|
||||
{
|
||||
func updateAspectRatio()
|
||||
{
|
||||
self.aspectRatioConstraint?.isActive = false
|
||||
|
||||
self.aspectRatioConstraint = self.imageView.widthAnchor.constraint(equalTo: self.imageView.heightAnchor, multiplier: self.aspectRatio.width / self.aspectRatio.height)
|
||||
self.aspectRatioConstraint?.isActive = true
|
||||
|
||||
let aspectRatio: Double
|
||||
if self.aspectRatio.width > self.aspectRatio.height
|
||||
{
|
||||
aspectRatio = self.aspectRatio.height / self.aspectRatio.width
|
||||
}
|
||||
else
|
||||
{
|
||||
aspectRatio = self.aspectRatio.width / self.aspectRatio.height
|
||||
}
|
||||
|
||||
let tolerance = 0.001 as Double
|
||||
let modernAspectRatio = AppScreenshot.defaultAspectRatio.width / AppScreenshot.defaultAspectRatio.height
|
||||
|
||||
let isRounded = (aspectRatio >= modernAspectRatio - tolerance) && (aspectRatio <= modernAspectRatio + tolerance)
|
||||
self.isRounded = isRounded
|
||||
}
|
||||
|
||||
func updateTraits()
|
||||
{
|
||||
let displayScale = (self.traitCollection.displayScale == 0.0) ? 1.0 : self.traitCollection.displayScale
|
||||
self.imageView.layer.borderWidth = 1.0 / displayScale
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
//
|
||||
// AppScreenshotsViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/18/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
class AppScreenshotsViewController: UICollectionViewController
|
||||
{
|
||||
let app: StoreApp
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
init?(app: StoreApp, coder: NSCoder)
|
||||
{
|
||||
self.app = app
|
||||
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.collectionView.showsHorizontalScrollIndicator = false
|
||||
|
||||
// Allow parent background color to show through.
|
||||
self.collectionView.backgroundColor = nil
|
||||
|
||||
// Match the parent table view margins.
|
||||
self.collectionView.directionalLayoutMargins.top = 0
|
||||
self.collectionView.directionalLayoutMargins.bottom = 0
|
||||
self.collectionView.directionalLayoutMargins.leading = 20
|
||||
self.collectionView.directionalLayoutMargins.trailing = 20
|
||||
|
||||
let collectionViewLayout = self.makeLayout()
|
||||
self.collectionView.collectionViewLayout = collectionViewLayout
|
||||
|
||||
self.collectionView.register(AppScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
self.collectionView.dataSource = self.dataSource
|
||||
self.collectionView.prefetchDataSource = self.dataSource
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppScreenshotsViewController
|
||||
{
|
||||
func makeLayout() -> UICollectionViewCompositionalLayout
|
||||
{
|
||||
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration()
|
||||
layoutConfig.contentInsetsReference = .layoutMargins
|
||||
|
||||
let preferredHeight = 400.0
|
||||
let estimatedWidth = preferredHeight * (AppScreenshot.defaultAspectRatio.width / AppScreenshot.defaultAspectRatio.height)
|
||||
|
||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
||||
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(estimatedWidth), heightDimension: .fractionalHeight(1.0))
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .estimated(estimatedWidth), heightDimension: .absolute(preferredHeight))
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||
|
||||
let layoutSection = NSCollectionLayoutSection(group: group)
|
||||
layoutSection.interGroupSpacing = 10
|
||||
layoutSection.orthogonalScrollingBehavior = .groupPaging
|
||||
|
||||
return layoutSection
|
||||
}, configuration: layoutConfig)
|
||||
|
||||
return layout
|
||||
}
|
||||
|
||||
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: self.app.screenshots)
|
||||
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
||||
let cell = cell as! AppScreenshotCollectionViewCell
|
||||
cell.imageView.image = nil
|
||||
cell.imageView.isIndicatingActivity = true
|
||||
|
||||
var aspectRatio = screenshot.size ?? AppScreenshot.defaultAspectRatio
|
||||
if aspectRatio.width > aspectRatio.height
|
||||
{
|
||||
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
|
||||
}
|
||||
|
||||
cell.aspectRatio = aspectRatio
|
||||
}
|
||||
dataSource.prefetchHandler = { (screenshot, indexPath, completionHandler) in
|
||||
let imageURL = screenshot.imageURL
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
let request = ImageRequest(url: imageURL, processors: [.screenshot])
|
||||
ImagePipeline.shared.loadImage(with: request, progress: nil) { result in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .success(let response): completionHandler(response.image, nil)
|
||||
case .failure(let error): completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! AppScreenshotCollectionViewCell
|
||||
cell.imageView.isIndicatingActivity = false
|
||||
cell.imageView.image = image
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview(traits: .portrait) {
|
||||
DatabaseManager.shared.startForPreview()
|
||||
|
||||
let fetchRequest = StoreApp.fetchRequest()
|
||||
let storeApp = try! DatabaseManager.shared.viewContext.fetch(fetchRequest).first!
|
||||
|
||||
let storyboard = UIStoryboard(name: "Main", bundle: .main)
|
||||
let appViewConttroller = storyboard.instantiateViewController(withIdentifier: "appViewController") as! AppViewController
|
||||
appViewConttroller.app = storeApp
|
||||
|
||||
let navigationController = UINavigationController(rootViewController: appViewConttroller)
|
||||
return navigationController
|
||||
}
|
||||
Reference in New Issue
Block a user