diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 39f08536..62075ca3 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -418,6 +418,7 @@ D5A299892AAB9E5900A3988D /* AppProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A6B7D2AA9226C00F61259 /* AppProcess.swift */; }; D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */; }; 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 */; }; D5C8ACDB2A956B2B00669F92 /* Process+STPrivilegedTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C8ACDA2A956B2B00669F92 /* Process+STPrivilegedTask.swift */; }; D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4A280E141900469595 /* ManagedPatron.swift */; }; @@ -1083,6 +1084,7 @@ D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedAPIs.swift; sourceTree = ""; }; D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearAppCacheOperation.swift; sourceTree = ""; }; D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+Previews.swift"; sourceTree = ""; }; + D5B6F6AA2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewAppScreenshotsViewController.swift; sourceTree = ""; }; D5BA9E9A2A9FE1E8007C0661 /* JITManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JITManager.swift; sourceTree = ""; }; D5C8ACDA2A956B2B00669F92 /* Process+STPrivilegedTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Process+STPrivilegedTask.swift"; sourceTree = ""; }; D5CA0C4A280E141900469595 /* ManagedPatron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedPatron.swift; sourceTree = ""; }; @@ -2219,6 +2221,7 @@ isa = PBXGroup; children = ( D5F982202AB910180045751F /* AppScreenshotsViewController.swift */, + D5B6F6AA2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift */, D5418F162AD740890014ABD6 /* AppScreenshotCollectionViewCell.swift */, ); path = Screenshots; @@ -3214,6 +3217,7 @@ BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */, BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */, D5CF568C2A0D8EEC006D93E2 /* VerificationError.swift in Sources */, + D5B6F6AB2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift in Sources */, D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */, diff --git a/AltStore/App Detail/AppViewController.swift b/AltStore/App Detail/AppViewController.swift index 67aa547d..44c26f42 100644 --- a/AltStore/App Detail/AppViewController.swift +++ b/AltStore/App Detail/AppViewController.swift @@ -142,6 +142,14 @@ final class AppViewController: UIViewController self.view.layoutIfNeeded() } + override func viewIsAppearing(_ animated: Bool) + { + super.viewIsAppearing(animated) + + // Prevent banner temporarily flashing a color due to being added back to self.view. + self.bannerView.backgroundEffectView.backgroundColor = .clear + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -191,21 +199,7 @@ final class AppViewController: UIViewController self._shouldResetLayout = false } - let statusBarHeight: Double - - if let navigationController, navigationController.presentingViewController != nil, navigationController.modalPresentationStyle != .fullScreen - { - statusBarHeight = 20 - } - else if let statusBarManager = self.view.window?.windowScene?.statusBarManager - { - statusBarHeight = statusBarManager.statusBarFrame.height - } - else - { - statusBarHeight = 0 - } - + let statusBarHeight = (self.view.window ?? self.presentedViewController?.view.window)?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0 let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius let inset = 12 as CGFloat diff --git a/AltStore/App Detail/Screenshots/AppScreenshotsViewController.swift b/AltStore/App Detail/Screenshots/AppScreenshotsViewController.swift index 99c2c950..56e3976a 100644 --- a/AltStore/App Detail/Screenshots/AppScreenshotsViewController.swift +++ b/AltStore/App Detail/Screenshots/AppScreenshotsViewController.swift @@ -129,6 +129,21 @@ private extension AppScreenshotsViewController } } +extension AppScreenshotsViewController +{ + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) + { + let screenshot = self.dataSource.item(at: indexPath) + + let previewViewController = PreviewAppScreenshotsViewController(app: self.app) + previewViewController.currentScreenshot = screenshot + + let navigationController = UINavigationController(rootViewController: previewViewController) + navigationController.modalPresentationStyle = .fullScreen + self.present(navigationController, animated: true) + } +} + @available(iOS 17, *) #Preview(traits: .portrait) { DatabaseManager.shared.startForPreview() diff --git a/AltStore/App Detail/Screenshots/PreviewAppScreenshotsViewController.swift b/AltStore/App Detail/Screenshots/PreviewAppScreenshotsViewController.swift new file mode 100644 index 00000000..9dd40797 --- /dev/null +++ b/AltStore/App Detail/Screenshots/PreviewAppScreenshotsViewController.swift @@ -0,0 +1,164 @@ +// +// PreviewAppScreenshotsViewController.swift +// AltStore +// +// Created by Riley Testut on 9/19/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import UIKit + +import AltStoreCore +import Roxas + +import Nuke + +class PreviewAppScreenshotsViewController: UICollectionViewController +{ + let app: StoreApp + + var currentScreenshot: AppScreenshot? + + private lazy var dataSource = self.makeDataSource() + + init(app: StoreApp) + { + self.app = app + + super.init(collectionViewLayout: UICollectionViewFlowLayout()) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() + { + super.viewDidLoad() + + let tintColor = self.app.tintColor ?? .altPrimary + self.navigationController?.view.tintColor = tintColor + + self.view.backgroundColor = .systemBackground + self.collectionView.backgroundColor = nil + + let collectionViewLayout = self.makeLayout() + self.collectionView.collectionViewLayout = collectionViewLayout + + self.collectionView.directionalLayoutMargins.leading = 20 + self.collectionView.directionalLayoutMargins.trailing = 20 + + self.collectionView.preservesSuperviewLayoutMargins = true + self.collectionView.insetsLayoutMarginsFromSafeArea = true + + self.collectionView.alwaysBounceVertical = false + self.collectionView.register(AppScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier) + + self.collectionView.dataSource = self.dataSource + self.collectionView.prefetchDataSource = self.dataSource + + let doneButton = UIBarButtonItem(systemItem: .done, primaryAction: UIAction { [weak self] _ in + self?.dismiss(animated: true) + }) + self.navigationItem.rightBarButtonItem = doneButton + } + + override func viewIsAppearing(_ animated: Bool) + { + super.viewIsAppearing(animated) + + if let screenshot = self.currentScreenshot, let index = self.dataSource.items.firstIndex(of: screenshot) + { + let indexPath = IndexPath(item: index, section: 0) + self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) + } + } +} + +private extension PreviewAppScreenshotsViewController +{ + func makeLayout() -> UICollectionViewCompositionalLayout + { + let layoutConfig = UICollectionViewCompositionalLayoutConfiguration() + layoutConfig.contentInsetsReference = .none + + let layout = UICollectionViewCompositionalLayout(sectionProvider: { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in + guard let self else { return nil } + + let contentInsets = self.collectionView.directionalLayoutMargins + let groupWidth = layoutEnvironment.container.contentSize.width - (contentInsets.leading + contentInsets.trailing) + let groupHeight = layoutEnvironment.container.contentSize.height - (contentInsets.top + contentInsets.bottom) + + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(groupWidth), heightDimension: .absolute(groupHeight)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + + let layoutSection = NSCollectionLayoutSection(group: group) + layoutSection.orthogonalScrollingBehavior = .groupPagingCentered + layoutSection.interGroupSpacing = 10 + return layoutSection + }, configuration: layoutConfig) + + return layout + } + + func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource + { + let dataSource = RSTArrayCollectionViewPrefetchingDataSource(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 previewViewController = PreviewAppScreenshotsViewController(app: storeApp) + + let navigationController = UINavigationController(rootViewController: previewViewController) + return navigationController +}