mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Replaces BrowseCollectionViewCell with AppCardCollectionViewCell
* Handles dynamic screenshot sizes * Allows swiping through screenshots * Supports iPhone + iPad screenshots
This commit is contained in:
@@ -245,7 +245,6 @@
|
||||
BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */; };
|
||||
BF989185250AAD1D002ACF50 /* UIColor+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */; };
|
||||
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */; };
|
||||
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */; };
|
||||
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */; };
|
||||
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4A22DD137F008935CF /* NavigationBar.swift */; };
|
||||
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4C22DD16DE008935CF /* PillButton.swift */; };
|
||||
@@ -293,7 +292,6 @@
|
||||
BFD52C2122A1A9EC000B7ED1 /* node_list.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1E22A1A9EC000B7ED1 /* node_list.c */; };
|
||||
BFD52C2222A1A9EC000B7ED1 /* cnary.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1F22A1A9EC000B7ED1 /* cnary.c */; };
|
||||
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */; };
|
||||
BFDB5B2622EFBBEA00F74113 /* BrowseCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */; };
|
||||
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */; };
|
||||
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */; };
|
||||
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */; };
|
||||
@@ -420,6 +418,7 @@
|
||||
D5B6F6A92AD75D01007EED5A /* ProcessInfo+Previews.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */; };
|
||||
D5B6F6AB2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6F6AA2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift */; };
|
||||
D5BA9E9B2A9FE1E8007C0661 /* JITManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5BA9E9A2A9FE1E8007C0661 /* JITManager.swift */; };
|
||||
D5C0E7672AD9C75900530CA4 /* AppCardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C0E7662AD9C75900530CA4 /* AppCardCollectionViewCell.swift */; };
|
||||
D5C8ACDB2A956B2B00669F92 /* Process+STPrivilegedTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C8ACDA2A956B2B00669F92 /* Process+STPrivilegedTask.swift */; };
|
||||
D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4A280E141900469595 /* ManagedPatron.swift */; };
|
||||
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */; };
|
||||
@@ -935,7 +934,6 @@
|
||||
BF989190250AAE86002ACF50 /* ViewAppIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewAppIntentHandler.swift; sourceTree = "<group>"; };
|
||||
BF989191250AAE86002ACF50 /* ViewApp.intentdefinition */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.intentdefinition; path = ViewApp.intentdefinition; sourceTree = "<group>"; };
|
||||
BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseViewController.swift; sourceTree = "<group>"; };
|
||||
BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
BF9ABA4A22DD137F008935CF /* NavigationBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = "<group>"; };
|
||||
BF9ABA4C22DD16DE008935CF /* PillButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButton.swift; sourceTree = "<group>"; };
|
||||
@@ -979,7 +977,6 @@
|
||||
BFD52C1F22A1A9EC000B7ED1 /* cnary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cnary.c; path = Dependencies/libplist/libcnary/cnary.c; sourceTree = SOURCE_ROOT; };
|
||||
BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsComponents.swift; sourceTree = "<group>"; };
|
||||
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+RelativeDate.swift"; sourceTree = "<group>"; };
|
||||
BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BrowseCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResignAppOperation.swift; sourceTree = "<group>"; };
|
||||
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = "<group>"; };
|
||||
BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationError.swift; sourceTree = "<group>"; };
|
||||
@@ -1085,6 +1082,7 @@
|
||||
D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+Previews.swift"; sourceTree = "<group>"; };
|
||||
D5B6F6AA2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewAppScreenshotsViewController.swift; sourceTree = "<group>"; };
|
||||
D5BA9E9A2A9FE1E8007C0661 /* JITManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JITManager.swift; sourceTree = "<group>"; };
|
||||
D5C0E7662AD9C75900530CA4 /* AppCardCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCardCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D5C8ACDA2A956B2B00669F92 /* Process+STPrivilegedTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Process+STPrivilegedTask.swift"; sourceTree = "<group>"; };
|
||||
D5CA0C4A280E141900469595 /* ManagedPatron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedPatron.swift; sourceTree = "<group>"; };
|
||||
D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 10.xcdatamodel"; sourceTree = "<group>"; };
|
||||
@@ -1815,8 +1813,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */,
|
||||
BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */,
|
||||
BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */,
|
||||
BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */,
|
||||
);
|
||||
path = Browse;
|
||||
@@ -1989,6 +1985,7 @@
|
||||
BF6C8FAD2429597900125131 /* AppBannerCollectionViewCell.swift */,
|
||||
BF6C8FAF2429599900125131 /* TextCollectionReusableView.swift */,
|
||||
D5CD805C29CA2C1E00E591B0 /* HeaderContentViewController.swift */,
|
||||
D5C0E7662AD9C75900530CA4 /* AppCardCollectionViewCell.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@@ -3234,7 +3231,6 @@
|
||||
BD4513AB2C6FA98C0052BCC0 /* AppExtensionView.swift in Sources */,
|
||||
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
|
||||
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */,
|
||||
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */,
|
||||
D552B1D82A042A740066216F /* AppPermissionsCard.swift in Sources */,
|
||||
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */,
|
||||
BF41B808233433C100C593A3 /* LoadingState.swift in Sources */,
|
||||
@@ -3279,6 +3275,7 @@
|
||||
BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */,
|
||||
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
|
||||
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
|
||||
D5C0E7672AD9C75900530CA4 /* AppCardCollectionViewCell.swift in Sources */,
|
||||
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */,
|
||||
D54058BB2A1D8FE3008CCC58 /* UIColor+AltStore.swift in Sources */,
|
||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
|
||||
|
||||
@@ -10,6 +10,20 @@ import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension AppScreenshotCollectionViewCell
|
||||
{
|
||||
private class ImageView: UIImageView
|
||||
{
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
// Explicitly layout cell to ensure rounded corners are accurate.
|
||||
self.superview?.superview?.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppScreenshotCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
let imageView: UIImageView
|
||||
@@ -31,7 +45,7 @@ class AppScreenshotCollectionViewCell: UICollectionViewCell
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
self.imageView = UIImageView(frame: .zero)
|
||||
self.imageView = ImageView(frame: .zero)
|
||||
self.imageView.clipsToBounds = true
|
||||
self.imageView.layer.cornerCurve = .continuous
|
||||
self.imageView.layer.borderColor = UIColor.tertiaryLabel.cgColor
|
||||
@@ -42,10 +56,10 @@ class AppScreenshotCollectionViewCell: UICollectionViewCell
|
||||
self.contentView.addSubview(self.imageView)
|
||||
|
||||
let widthConstraint = self.imageView.widthAnchor.constraint(equalTo: self.contentView.widthAnchor)
|
||||
widthConstraint.priority = UILayoutPriority(999)
|
||||
widthConstraint.priority = .defaultHigh
|
||||
|
||||
let heightConstraint = self.imageView.heightAnchor.constraint(equalTo: self.contentView.heightAnchor)
|
||||
heightConstraint.priority = UILayoutPriority(999)
|
||||
heightConstraint.priority = .defaultHigh
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
widthConstraint,
|
||||
@@ -75,11 +89,6 @@ class AppScreenshotCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
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
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<!--Browse-->
|
||||
<scene sceneID="rXq-UR-qQp">
|
||||
<objects>
|
||||
<collectionViewController id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionViewController storyboardIdentifier="browseViewController" id="e3L-BF-iXp" customClass="BrowseViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
|
||||
@@ -22,7 +22,7 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
||||
|
||||
private let prototypeCell = BrowseCollectionViewCell.instantiate(with: BrowseCollectionViewCell.nib!)!
|
||||
private let prototypeCell = AppCardCollectionViewCell(frame: .zero)
|
||||
|
||||
private var loadingState: LoadingState = .loading {
|
||||
didSet {
|
||||
@@ -59,7 +59,7 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
||||
|
||||
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
self.collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
self.collectionView.register(AppCardCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
self.collectionView.dataSource = self.dataSource
|
||||
self.collectionView.prefetchDataSource = self.dataSource
|
||||
@@ -121,14 +121,11 @@ private extension BrowseViewController
|
||||
let context = self.source?.managedObjectContext ?? DatabaseManager.shared.viewContext
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: context)
|
||||
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
|
||||
let cell = cell as! BrowseCollectionViewCell
|
||||
let cell = cell as! AppCardCollectionViewCell
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
cell.subtitleLabel.text = app.subtitle
|
||||
cell.imageURLs = Array(app.screenshotURLs.prefix(2))
|
||||
|
||||
cell.bannerView.configure(for: app)
|
||||
cell.configure(for: app)
|
||||
|
||||
cell.bannerView.iconImageView.image = nil
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||
@@ -188,7 +185,7 @@ private extension BrowseViewController
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! BrowseCollectionViewCell
|
||||
let cell = cell as! AppCardCollectionViewCell
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
cell.bannerView.iconImageView.image = image
|
||||
|
||||
@@ -365,21 +362,18 @@ extension BrowseViewController: UICollectionViewDelegateFlowLayout
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||
{
|
||||
let item = self.dataSource.item(at: indexPath)
|
||||
let itemID = item.globallyUniqueID ?? item.bundleIdentifier
|
||||
|
||||
if let previousSize = self.cachedItemSizes[item.bundleIdentifier]
|
||||
if let previousSize = self.cachedItemSizes[itemID]
|
||||
{
|
||||
return previousSize
|
||||
}
|
||||
|
||||
let maxVisibleScreenshots = 2 as CGFloat
|
||||
let aspectRatio: CGFloat = 16.0 / 9.0
|
||||
|
||||
let layout = self.prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||
let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right
|
||||
|
||||
self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
|
||||
|
||||
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
|
||||
let insets = (self.view.layoutMargins.left + self.view.layoutMargins.right)
|
||||
|
||||
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width - insets)
|
||||
widthConstraint.isActive = true
|
||||
defer { widthConstraint.isActive = false }
|
||||
|
||||
@@ -387,17 +381,8 @@ extension BrowseViewController: UICollectionViewDelegateFlowLayout
|
||||
self.prototypeCell.frame.size.width = widthConstraint.constant
|
||||
self.prototypeCell.layoutIfNeeded()
|
||||
|
||||
let collectionViewWidth = self.prototypeCell.screenshotsCollectionView.bounds.width
|
||||
let screenshotWidth = ((collectionViewWidth - padding) / maxVisibleScreenshots).rounded(.down)
|
||||
let screenshotHeight = screenshotWidth * aspectRatio
|
||||
|
||||
let heightConstraint = self.prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight)
|
||||
heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error.
|
||||
heightConstraint.isActive = true
|
||||
defer { heightConstraint.isActive = false }
|
||||
|
||||
let itemSize = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
self.cachedItemSizes[item.bundleIdentifier] = itemSize
|
||||
self.cachedItemSizes[itemID] = itemSize
|
||||
return itemSize
|
||||
}
|
||||
|
||||
@@ -434,3 +419,16 @@ extension BrowseViewController: UIViewControllerPreviewingDelegate
|
||||
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview(traits: .portrait) {
|
||||
DatabaseManager.shared.startForPreview()
|
||||
|
||||
let storyboard = UIStoryboard(name: "Main", bundle: .main)
|
||||
let browseViewController = storyboard.instantiateViewController(identifier: "browseViewController") { coder in
|
||||
BrowseViewController(source: nil, coder: coder)
|
||||
}
|
||||
|
||||
let navigationController = UINavigationController(rootViewController: browseViewController)
|
||||
return navigationController
|
||||
}
|
||||
|
||||
276
AltStore/Components/AppCardCollectionViewCell.swift
Normal file
276
AltStore/Components/AppCardCollectionViewCell.swift
Normal file
@@ -0,0 +1,276 @@
|
||||
//
|
||||
// AppCardCollectionViewCell.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/13/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
private let minimumItemSpacing = 8.0
|
||||
|
||||
class AppCardCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
let bannerView: AppBannerView
|
||||
|
||||
private let screenshotsCollectionView: UICollectionView
|
||||
private let stackView: UIStackView
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
private var screenshots: [AppScreenshot] = [] {
|
||||
didSet {
|
||||
self.dataSource.items = self.screenshots
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
self.bannerView = AppBannerView(frame: .zero)
|
||||
|
||||
self.screenshotsCollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
|
||||
self.screenshotsCollectionView.backgroundColor = nil
|
||||
self.screenshotsCollectionView.alwaysBounceVertical = false
|
||||
self.screenshotsCollectionView.alwaysBounceHorizontal = true
|
||||
self.screenshotsCollectionView.showsHorizontalScrollIndicator = false
|
||||
self.screenshotsCollectionView.showsVerticalScrollIndicator = false
|
||||
|
||||
self.stackView = UIStackView(arrangedSubviews: [self.bannerView, self.screenshotsCollectionView])
|
||||
self.stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.stackView.spacing = 0
|
||||
self.stackView.axis = .vertical
|
||||
self.stackView.alignment = .fill
|
||||
self.stackView.distribution = .equalSpacing
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.contentView.clipsToBounds = true
|
||||
self.contentView.layer.cornerCurve = .continuous
|
||||
|
||||
self.contentView.addSubview(self.bannerView.backgroundEffectView, pinningEdgesWith: .zero)
|
||||
self.contentView.addSubview(self.stackView, pinningEdgesWith: .zero)
|
||||
|
||||
self.screenshotsCollectionView.collectionViewLayout = self.makeLayout()
|
||||
self.screenshotsCollectionView.dataSource = self.dataSource
|
||||
self.screenshotsCollectionView.prefetchDataSource = self.dataSource
|
||||
|
||||
// Adding screenshotsCollectionView's gesture recognizers to self.contentView breaks paging,
|
||||
// so instead we intercept taps and pass them onto delegate.
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(AppCardCollectionViewCell.handleTapGesture(_:)))
|
||||
tapGestureRecognizer.cancelsTouchesInView = false
|
||||
tapGestureRecognizer.delaysTouchesBegan = false
|
||||
tapGestureRecognizer.delaysTouchesEnded = false
|
||||
self.screenshotsCollectionView.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
||||
self.screenshotsCollectionView.register(AppScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
let inset = 14.0 //TODO: Assign from bannerView's layoutMargins
|
||||
self.stackView.isLayoutMarginsRelativeArrangement = true
|
||||
self.stackView.layoutMargins.bottom = inset
|
||||
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
self.screenshotsCollectionView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 0, leading: inset, bottom: 0, trailing: inset)
|
||||
|
||||
// Aspect ratio constraint to fit exactly 3 modern portrait iPhone screenshots side-by-side (with spacing).
|
||||
let multiplier = (AppScreenshot.defaultAspectRatio.width * 3) / AppScreenshot.defaultAspectRatio.height
|
||||
let spacing = (inset * 2) + (minimumItemSpacing * 2)
|
||||
let aspectRatioConstraint = self.screenshotsCollectionView.widthAnchor.constraint(equalTo: self.screenshotsCollectionView.heightAnchor, multiplier: multiplier, constant: spacing)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
aspectRatioConstraint,
|
||||
self.bannerView.heightAnchor.constraint(equalToConstant: 88)
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
self.contentView.layer.cornerRadius = self.bannerView.layer.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppCardCollectionViewCell
|
||||
{
|
||||
func makeLayout() -> UICollectionViewCompositionalLayout
|
||||
{
|
||||
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration()
|
||||
layoutConfig.contentInsetsReference = .layoutMargins
|
||||
|
||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
||||
guard let self else { return nil }
|
||||
|
||||
var contentWidth = 0.0
|
||||
var numberOfVisibleScreenshots = 0
|
||||
|
||||
for screenshot in self.screenshots
|
||||
{
|
||||
var aspectRatio = screenshot.aspectRatio
|
||||
if aspectRatio.width > aspectRatio.height
|
||||
{
|
||||
switch screenshot.deviceType
|
||||
{
|
||||
case .iphone:
|
||||
// Always rotate landscape iPhone screenshots
|
||||
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
|
||||
|
||||
case .ipad:
|
||||
// Never rotate iPad screenshots
|
||||
break
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
let screenshotWidth = (layoutEnvironment.container.effectiveContentSize.height * (aspectRatio.width / aspectRatio.height)).rounded(.up) // Round to ensure we over-estimate contentWidth.
|
||||
|
||||
let totalContentWidth = contentWidth + (screenshotWidth + minimumItemSpacing)
|
||||
if totalContentWidth > layoutEnvironment.container.effectiveContentSize.width
|
||||
{
|
||||
// totalContentWidth is larger than visible width.
|
||||
break
|
||||
}
|
||||
|
||||
contentWidth = totalContentWidth
|
||||
numberOfVisibleScreenshots += 1
|
||||
}
|
||||
|
||||
// Use .estimated(1) to ensure we don't over-estimate widths, which can cause incorrect layouts for the last group.
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(1), heightDimension: .fractionalHeight(1.0))
|
||||
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(0), top: nil, trailing: nil, bottom: nil)
|
||||
|
||||
let groupItem = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let trailingGroup = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [groupItem])
|
||||
trailingGroup.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: nil, trailing: .flexible(0), bottom: nil)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, trailingGroup])
|
||||
group.interItemSpacing = .fixed(minimumItemSpacing)
|
||||
|
||||
if numberOfVisibleScreenshots < self.screenshots.count
|
||||
{
|
||||
// There are more screenshots than what is displayed, so no need to manually center them.
|
||||
}
|
||||
else
|
||||
{
|
||||
// We're showing all screenshots initially, so make sure they're centered.
|
||||
|
||||
let insetWidth = (layoutEnvironment.container.effectiveContentSize.width - contentWidth) / 2.0
|
||||
group.contentInsets.leading = (insetWidth - 1).rounded(.down) // Subtract 1 to avoid overflowing/clipping
|
||||
}
|
||||
|
||||
let layoutSection = NSCollectionLayoutSection(group: group)
|
||||
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
|
||||
layoutSection.interGroupSpacing = self.screenshotsCollectionView.directionalLayoutMargins.leading + self.screenshotsCollectionView.directionalLayoutMargins.trailing
|
||||
return layoutSection
|
||||
}, configuration: layoutConfig)
|
||||
|
||||
return layout
|
||||
}
|
||||
|
||||
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: [])
|
||||
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
||||
let cell = cell as! AppScreenshotCollectionViewCell
|
||||
cell.imageView.image = nil
|
||||
cell.imageView.isIndicatingActivity = true
|
||||
|
||||
var aspectRatio = screenshot.aspectRatio
|
||||
if aspectRatio.width > aspectRatio.height
|
||||
{
|
||||
switch screenshot.deviceType
|
||||
{
|
||||
case .iphone:
|
||||
// Always rotate landscape iPhone screenshots
|
||||
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
|
||||
|
||||
case .ipad:
|
||||
// Never rotate iPad screenshots
|
||||
break
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
cell.aspectRatio = aspectRatio
|
||||
}
|
||||
dataSource.prefetchHandler = { (screenshot, indexPath, completionHandler) in
|
||||
let imageURL = screenshot.imageURL
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
let request = ImageRequest(url: imageURL)
|
||||
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.setImage(image)
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
@objc func handleTapGesture(_ tapGesture: UITapGestureRecognizer)
|
||||
{
|
||||
var superview: UIView? = self.superview
|
||||
var collectionView: UICollectionView? = nil
|
||||
|
||||
while case let view? = superview
|
||||
{
|
||||
if let cv = view as? UICollectionView
|
||||
{
|
||||
collectionView = cv
|
||||
break
|
||||
}
|
||||
|
||||
superview = view.superview
|
||||
}
|
||||
|
||||
if let collectionView, let indexPath = collectionView.indexPath(for: self)
|
||||
{
|
||||
collectionView.delegate?.collectionView?(collectionView, didSelectItemAt: indexPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppCardCollectionViewCell
|
||||
{
|
||||
func configure(for storeApp: StoreApp)
|
||||
{
|
||||
self.screenshots = storeApp.preferredScreenshots()
|
||||
|
||||
self.bannerView.tintColor = storeApp.tintColor
|
||||
self.bannerView.configure(for: storeApp)
|
||||
|
||||
self.bannerView.subtitleLabel.numberOfLines = 1
|
||||
self.bannerView.subtitleLabel.lineBreakMode = .byTruncatingTail
|
||||
self.bannerView.subtitleLabel.minimumScaleFactor = 0.8
|
||||
self.bannerView.subtitleLabel.text = storeApp.subtitle ?? storeApp.developerName
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,13 @@ public class AppScreenshot: NSManagedObject, Fetchable, Decodable
|
||||
}
|
||||
}
|
||||
|
||||
public extension AppScreenshot
|
||||
{
|
||||
var aspectRatio: CGSize {
|
||||
return self.size ?? AppScreenshot.defaultAspectRatio
|
||||
}
|
||||
}
|
||||
|
||||
extension AppScreenshot
|
||||
{
|
||||
var screenshotID: String {
|
||||
|
||||
Reference in New Issue
Block a user