From 703db062e60a1e2ebd76b8897baf8bf9b8e5e4c1 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Wed, 6 Dec 2023 15:36:20 -0600 Subject: [PATCH] =?UTF-8?q?Shows=20source/category=20icon=20next=20to=20Br?= =?UTF-8?q?owseViewController=E2=80=99s=20title?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also tints all navigation bar buttons to match source/category tint color. # Conflicts: # AltStore/Browse/BrowseViewController.swift --- AltStore/Browse/BrowseViewController.swift | 155 ++++++++++++++++++--- AltStore/Components/AppIconImageView.swift | 18 ++- AltStoreCore/Types/StoreCategory.swift | 8 ++ 3 files changed, 158 insertions(+), 23 deletions(-) diff --git a/AltStore/Browse/BrowseViewController.swift b/AltStore/Browse/BrowseViewController.swift index 2b869272..973c0557 100644 --- a/AltStore/Browse/BrowseViewController.swift +++ b/AltStore/Browse/BrowseViewController.swift @@ -39,10 +39,17 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing private let prototypeCell = AppCardCollectionViewCell(frame: .zero) private var sortButton: UIBarButtonItem? + private var categoriesMenu: UIMenu? + private var preferredAppSorting: AppSorting = UserDefaults.shared.preferredAppSorting private var cancellables = Set() + private var titleStackView: UIStackView! + private var titleSourceIconView: AppIconImageView! + private var titleCategoryIconView: UIImageView! + private var titleLabel: UILabel! + init?(source: Source?, coder: NSCoder) { self.source = source @@ -78,6 +85,7 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing self.collectionView.backgroundColor = .altBackground self.collectionView.alwaysBounceVertical = true + self.dataSource.searchController.delegate = self self.dataSource.searchController.searchableKeyPaths = [#keyPath(StoreApp.name), #keyPath(StoreApp.subtitle), #keyPath(StoreApp.developerName), @@ -101,33 +109,31 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing }) self.collectionView.refreshControl = refreshControl - if let source = self.source + if self.category != nil, #available(iOS 16, *) { - let tintColor = source.effectiveTintColor ?? .altPrimary - self.view.tintColor = tintColor - - let appearance = NavigationBarAppearance() - appearance.configureWithTintColor(tintColor) - appearance.configureWithDefaultBackground() - - let edgeAppearance = appearance.copy() - edgeAppearance.configureWithTransparentBackground() - - self.navigationItem.standardAppearance = appearance - self.navigationItem.scrollEdgeAppearance = edgeAppearance - } - else if self.category != nil, #available(iOS 16, *) - { - let menu = UIMenu(children: [ + let categoriesMenu = UIMenu(children: [ UIDeferredMenuElement.uncached { [weak self] completion in let actions = self?.makeCategoryActions() ?? [] completion(actions) } ]) - self.navigationItem.titleMenuProvider = { _ in menu } + self.navigationItem.titleMenuProvider = { _ in categoriesMenu } + + self.categoriesMenu = categoriesMenu } + self.titleSourceIconView = AppIconImageView(style: .circular) + + self.titleCategoryIconView = UIImageView(frame: .zero) + self.titleCategoryIconView.contentMode = .scaleAspectFit + + self.titleLabel = UILabel() + self.titleLabel.font = UIFont.preferredFont(forTextStyle: .headline) + + self.titleStackView = UIStackView(arrangedSubviews: [self.titleSourceIconView, self.titleCategoryIconView, self.titleLabel]) + self.titleStackView.spacing = 4 + self.navigationItem.largeTitleDisplayMode = .never if #available(iOS 16, *) @@ -142,6 +148,15 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing self.preparePipeline() + NSLayoutConstraint.activate([ + // Source icon = equal width and height + self.titleSourceIconView.heightAnchor.constraint(equalToConstant: 26), + self.titleSourceIconView.widthAnchor.constraint(equalTo: self.titleSourceIconView.heightAnchor), + + // Category icon = constant height, variable widths + self.titleCategoryIconView.heightAnchor.constraint(equalToConstant: 26) + ]) + self.updateDataSource() self.update() } @@ -152,6 +167,13 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing self.update() } + + override func viewDidDisappear(_ animated: Bool) + { + super.viewDidDisappear(animated) + + self.navigationController?.navigationBar.tintColor = nil + } } private extension BrowseViewController @@ -346,13 +368,74 @@ private extension BrowseViewController } } - if let category = self.category + let tintColor: UIColor + + if let source = self.source { + tintColor = source.effectiveTintColor?.adjustedForDisplay ?? .altPrimary + + self.title = source.name + + self.titleSourceIconView.backgroundColor = tintColor + self.titleSourceIconView.isHidden = false + + self.titleCategoryIconView.isHidden = true + + if let iconURL = source.effectiveIconURL + { + Nuke.loadImage(with: iconURL, into: self.titleSourceIconView) { result in + switch result + { + case .failure(let error): Logger.main.error("Failed to fetch source icon at \(iconURL, privacy: .public). \(error.localizedDescription, privacy: .public)") + case .success: self.titleSourceIconView.backgroundColor = .white + } + } + } + } + else if let category = self.category + { + tintColor = category.tintColor + self.title = category.localizedName + + let image = UIImage(systemName: category.filledSymbolName)?.withTintColor(tintColor, renderingMode: .alwaysOriginal) + self.titleCategoryIconView.image = image + self.titleCategoryIconView.isHidden = false + + self.titleSourceIconView.isHidden = true } else { + tintColor = .altPrimary + self.title = NSLocalizedString("Browse", comment: "") + + self.titleSourceIconView.isHidden = true + self.titleCategoryIconView.isHidden = true + } + + self.titleLabel.text = self.title + self.titleStackView.sizeToFit() + self.navigationItem.titleView = self.titleStackView + + self.view.tintColor = tintColor + + let appearance = NavigationBarAppearance() + appearance.configureWithTintColor(tintColor) + appearance.configureWithDefaultBackground() + + let edgeAppearance = appearance.copy() + edgeAppearance.configureWithTransparentBackground() + + self.navigationItem.standardAppearance = appearance + self.navigationItem.scrollEdgeAppearance = edgeAppearance + + // Necessary to tint UISearchController's inline bar button. + self.navigationController?.navigationBar.tintColor = tintColor + + if let sortButton + { + sortButton.image = sortButton.image?.withTintColor(tintColor, renderingMode: .alwaysOriginal) } } @@ -378,11 +461,17 @@ private extension BrowseViewController return StoreCategory(rawValue: rawCategory) ?? .other } - let sortedCategories = Set(categories).sorted(by: { $0.localizedName.localizedStandardCompare($1.localizedName) == .orderedAscending }) + var sortedCategories = Set(categories).sorted(by: { $0.localizedName.localizedStandardCompare($1.localizedName) == .orderedAscending }) + if let otherIndex = sortedCategories.firstIndex(of: .other) + { + // Ensure "Other" is always last + sortedCategories.move(fromOffsets: [otherIndex], toOffset: sortedCategories.count) + } let actions = sortedCategories.map { category in let state: UIAction.State = (category == self.category) ? .on : .off - return UIAction(title: category.localizedName, image: UIImage(systemName: category.symbolName), state: state) { _ in + let image = UIImage(systemName: category.symbolName)?.withTintColor(category.tintColor, renderingMode: .alwaysOriginal) + return UIAction(title: category.localizedName, image: image, state: state) { _ in handler(category) } } @@ -589,6 +678,30 @@ extension BrowseViewController: UIViewControllerPreviewingDelegate } } +extension BrowseViewController: UISearchControllerDelegate +{ + func willPresentSearchController(_ searchController: UISearchController) + { + // Hide titleView + menu to ensure search bar is as large as possible. + self.navigationItem.titleView = nil + + if #available(iOS 16, *) + { + self.navigationItem.titleMenuProvider = nil + } + } + + func willDismissSearchController(_ searchController: UISearchController) + { + self.navigationItem.titleView = self.titleStackView + + if let categoriesMenu, #available(iOS 16, *) + { + self.navigationItem.titleMenuProvider = { _ in categoriesMenu } + } + } +} + @available(iOS 17, *) #Preview(traits: .portrait) { DatabaseManager.shared.startForPreview() diff --git a/AltStore/Components/AppIconImageView.swift b/AltStore/Components/AppIconImageView.swift index 61462ed4..da18e25c 100644 --- a/AltStore/Components/AppIconImageView.swift +++ b/AltStore/Components/AppIconImageView.swift @@ -25,10 +25,24 @@ class AppIconImageView: UIImageView } } - override func awakeFromNib() + init(style: Style) { - super.awakeFromNib() + self.style = style + super.init(image: nil) + + self.initialize() + } + + required init?(coder: NSCoder) + { + super.init(coder: coder) + + self.initialize() + } + + private func initialize() + { self.contentMode = .scaleAspectFill self.clipsToBounds = true self.backgroundColor = .white diff --git a/AltStoreCore/Types/StoreCategory.swift b/AltStoreCore/Types/StoreCategory.swift index c5dda3b4..b2c29704 100644 --- a/AltStoreCore/Types/StoreCategory.swift +++ b/AltStoreCore/Types/StoreCategory.swift @@ -57,6 +57,14 @@ public enum StoreCategory: String, CaseIterable } } + public var filledSymbolName: String { + switch self + { + case .utilities: return self.symbolName + default: return self.symbolName + ".fill" + } + } + public var tintColor: UIColor { switch self {