Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AltStore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -4396,7 +4396,7 @@
SWIFT_OBJC_BRIDGING_HEADER = "AltStore/AltStore-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
Expand Down Expand Up @@ -4432,7 +4432,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "AltStore/AltStore-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
Expand Down
46 changes: 35 additions & 11 deletions AltStore/Browse/FeaturedViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class FeaturedViewController: UICollectionViewController

internal private(set) var searchController: RSTSearchController!
private var searchBrowseViewController: BrowseViewController!
private var lastLayoutWidth: CGFloat = 0

private var updateFediverseInteractionsResult: Result<Void, Error>?

Expand Down Expand Up @@ -121,12 +122,26 @@ class FeaturedViewController: UICollectionViewController
self.updateFediverseInteractionsIfNeeded()
}

override func viewDidAppear(_ animated: Bool)
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)

self.navigationController?.navigationBar.tintColor = .altPrimary
}

override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()

// Recompute the width-adaptive sections when the available width changes
// (rotation, sidebar collapse/expand) so columns don't stay stale until a
// tab switch.
if self.collectionView.bounds.width != self.lastLayoutWidth
{
self.lastLayoutWidth = self.collectionView.bounds.width
self.collectionView.collectionViewLayout.invalidateLayout()
}
}
}

private extension FeaturedViewController
Expand All @@ -149,28 +164,34 @@ private extension FeaturedViewController
case .recentlyUpdated:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(AppBannerView.standardHeight))
let item = NSCollectionLayoutItem(layoutSize: itemSize)

let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(AppBannerView.standardHeight * 2 + spacing))

// Cap the carousel column width so banners don't stretch edge-to-edge
// on iPad; multiple columns then peek in from the sides.
let columnWidth = min(layoutEnvironment.container.effectiveContentSize.width, 440)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(columnWidth), heightDimension: .absolute(AppBannerView.standardHeight * 2 + spacing))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item, item]) // 2 items per group
group.interItemSpacing = .fixed(spacing)

let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = spacing
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
layoutSection.orthogonalScrollingBehavior = .continuous
layoutSection.contentInsets.bottom = interSectionSpacing
layoutSection.boundarySupplementaryItems = [
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: titleSize, elementKind: ElementKind.sectionHeader.rawValue, alignment: .topLeading)
]
return layoutSection

case .categories:
let itemWidth = (layoutEnvironment.container.effectiveContentSize.width - spacing) / 2
// Adaptive number of columns by width (2 on iPhone / narrow, more on iPad).
let width = layoutEnvironment.container.effectiveContentSize.width
let columns = max(2, Int(width / 320))
let itemWidth = (width - spacing * CGFloat(columns - 1)) / CGFloat(columns)
let itemHeight = 90.0
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(itemWidth), heightDimension: .absolute(itemHeight))
let item = NSCollectionLayoutItem(layoutSize: itemSize)

let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(itemHeight))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, item]) // 2 items per group
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: Array(repeating: item, count: columns))
group.interItemSpacing = .fixed(spacing)

let layoutSection = NSCollectionLayoutSection(group: group)
Expand Down Expand Up @@ -201,8 +222,11 @@ private extension FeaturedViewController
let itemHeight: NSCollectionLayoutDimension = if #available(iOS 17, *) { .uniformAcrossSiblings(estimate: 350) } else { .estimated(350) }
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemHeight)
let item = NSCollectionLayoutItem(layoutSize: itemSize)

let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemHeight)

// Cap the featured card width so it doesn't stretch across an iPad;
// the paging carousel then shows the next card peeking in.
let cardWidth = min(layoutEnvironment.container.effectiveContentSize.width, 560)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(cardWidth), heightDimension: itemHeight)
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
group.interItemSpacing = .fixed(spacing)

Expand All @@ -213,7 +237,7 @@ private extension FeaturedViewController

let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = spacing
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
layoutSection.orthogonalScrollingBehavior = .continuous
layoutSection.contentInsets.top = 8
layoutSection.contentInsets.bottom = interSectionSpacing
layoutSection.boundarySupplementaryItems = [titleHeader, buttonHeader]
Expand Down
7 changes: 6 additions & 1 deletion AltStore/Components/HeaderContentViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,12 @@ class HeaderContentViewController<Header: UIView, Content: ScrollableContentView
}

let minimumContentHeight = minimumHeaderY + headerFrame.height + padding // Minimum height for header + back button + spacing.
let maximumContentY = max(self.view.bounds.width * 0.667, minimumContentHeight) // Initial Y-value of content view.

// The hero scales with width, which makes it enormous on a wide iPad. Halve
// the multiplier in regular widths (the `max(...)` below still guarantees room
// for the header + back button).
let heroHeightMultiplier = (self.traitCollection.horizontalSizeClass == .regular) ? 0.333 : 0.667
let maximumContentY = max(self.view.bounds.width * heroHeightMultiplier, minimumContentHeight) // Initial Y-value of content view.

contentFrame.origin.y = maximumContentY - self.scrollView.contentOffset.y
headerFrame.origin.y = contentFrame.origin.y - padding - headerFrame.height
Expand Down
98 changes: 97 additions & 1 deletion AltStore/My Apps/MyAppsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,13 @@ class MyAppsViewController: UICollectionViewController, PeekPopPreviewing
self.collectionView.register(UpdatesCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "UpdatesHeader")
self.collectionView.register(InstalledAppsCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "ActiveAppsHeader")
self.collectionView.register(InstalledAppsCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "InactiveAppsHeader")


// Use a compositional layout so the installed-apps grid has exact, even gaps and
// adapts its column count to the available width (1 column on iPhone, more on
// iPad). The storyboard's flow layout justified each row, which made the column
// gap wider than the row gap and stretched cells full-width on iPad.
self.collectionView.setCollectionViewLayout(self.makeLayout(), animated: false)

let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(MyAppsViewController.checkForUpdates(_:)), for: .primaryActionTriggered)
self.collectionView.refreshControl = refreshControl
Expand Down Expand Up @@ -2287,6 +2293,96 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
}
}

private extension MyAppsViewController
{
static let gridSpacing: CGFloat = 10

/// Compositional layout backing the installed-apps grid. Each row is divided into
/// N equal columns (N ≈ width / 350) with a fixed inter-item spacing that matches
/// the inter-group spacing, so the column and row gaps are always identical. The
/// updates section keeps its self-sizing cards, and the section headers/footers are
/// reproduced as boundary supplementary items (same reuse identifiers as before).
func makeLayout() -> UICollectionViewLayout
{
let spacing = MyAppsViewController.gridSpacing
let sectionInsets = NSDirectionalEdgeInsets(top: 12, leading: 0, bottom: 20, trailing: 0)

return UICollectionViewCompositionalLayout { [weak self] sectionIndex, layoutEnvironment in
guard let self else { return nil }

let section = Section.allCases[sectionIndex]
let width = layoutEnvironment.container.effectiveContentSize.width

switch section
{
case .noUpdates:
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(44))
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.contentInsetsReference = .safeArea
return layoutSection

case .updates:
// Update cards self-size via Auto Layout, so use an estimated height.
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(156))
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = spacing
layoutSection.contentInsets = sectionInsets
layoutSection.contentInsetsReference = .safeArea
if (self.updatesDataSource.fetchedResultsController.fetchedObjects?.count ?? 0) > Self.maximumCollapsedUpdatesCount
{
layoutSection.boundarySupplementaryItems = [
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(26)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
]
}
return layoutSection

case .activeApps, .inactiveApps:
let columns = max(1, Int(width / 350))
let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(88)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(88)), subitem: item, count: columns)
group.interItemSpacing = .fixed(spacing)

let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = spacing
layoutSection.contentInsets = sectionInsets
layoutSection.contentInsetsReference = .safeArea

var supplementaries: [NSCollectionLayoutBoundarySupplementaryItem] = []

// Header: active apps always show one; inactive apps only when populated
// (matches the old `referenceSizeForHeaderInSection`).
let showsHeader = (section == .activeApps) || self.inactiveAppsDataSource.itemCount > 0
if showsHeader
{
supplementaries.append(NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(29)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .top))
}

// The App IDs footer sits under whichever section is last (active when
// there are no inactive apps, otherwise inactive) and only with a team.
let hasTeam = DatabaseManager.shared.activeTeam() != nil
let showsFooter: Bool
switch section
{
case .activeApps: showsFooter = hasTeam && self.inactiveAppsDataSource.itemCount == 0
case .inactiveApps: showsFooter = hasTeam && self.inactiveAppsDataSource.itemCount > 0
default: showsFooter = false
}
if showsFooter
{
supplementaries.append(NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(80)), elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottom))
}

layoutSection.boundarySupplementaryItems = supplementaries
return layoutSection
}
}
}
}

extension MyAppsViewController: UICollectionViewDragDelegate
{
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]
Expand Down
Loading