Skip to content

Commit

Permalink
[UI] More diffable/compositional; offload more work to detached tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
kirb committed Jun 17, 2022
1 parent 5368bc6 commit 557dd17
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 241 deletions.
4 changes: 4 additions & 0 deletions Zebra.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
4E7DC8B128589B0C0007392D /* BaseListCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7DC8B028589B0C0007392D /* BaseListCollectionViewController.swift */; };
4E7DC8B32858A2990007392D /* ListCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7DC8B22858A2990007392D /* ListCollectionViewController.swift */; };
4E7DC8B5285A270E0007392D /* UIImage+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7DC8B4285A270E0007392D /* UIImage+Additions.swift */; };
4E7DC8B7285AF5120007392D /* PackageMenuCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7DC8B6285AF5120007392D /* PackageMenuCommands.swift */; };
4E7DC8B9285B3FE70007392D /* NetworkController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7DC8B8285B3FE70007392D /* NetworkController.swift */; };
4E840B3927D61ED700039938 /* SectionHeaderButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E840B3827D61ED700039938 /* SectionHeaderButton.swift */; };
4E840B3B27D6484900039938 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E840B3A27D6484900039938 /* GradientView.swift */; };
Expand Down Expand Up @@ -245,6 +246,7 @@
4E7DC8B028589B0C0007392D /* BaseListCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseListCollectionViewController.swift; sourceTree = "<group>"; };
4E7DC8B22858A2990007392D /* ListCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCollectionViewController.swift; sourceTree = "<group>"; };
4E7DC8B4285A270E0007392D /* UIImage+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Additions.swift"; sourceTree = "<group>"; };
4E7DC8B6285AF5120007392D /* PackageMenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageMenuCommands.swift; sourceTree = "<group>"; };
4E7DC8B8285B3FE70007392D /* NetworkController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkController.swift; sourceTree = "<group>"; };
4E840B3827D61ED700039938 /* SectionHeaderButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeaderButton.swift; sourceTree = "<group>"; };
4E840B3A27D6484900039938 /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -812,6 +814,7 @@
4E1F060728411A47006B3F0C /* PackageViewController.swift */,
4E1F060928411A93006B3F0C /* PackageListViewController.swift */,
4E1F061228475943006B3F0C /* PackageCollectionViewCell.swift */,
4E7DC8B6285AF5120007392D /* PackageMenuCommands.swift */,
);
path = Packages;
sourceTree = "<group>";
Expand Down Expand Up @@ -1542,6 +1545,7 @@
4EB23512283DFA5500713CBB /* Bzip2Decompressor.swift in Sources */,
4E840B3927D61ED700039938 /* SectionHeaderButton.swift in Sources */,
4EB2354F283DFB8B00713CBB /* ZstdDecompressor.swift in Sources */,
4E7DC8B7285AF5120007392D /* PackageMenuCommands.swift in Sources */,
4EC7F2A027B53D990078F953 /* FileImportController.swift in Sources */,
4EBA841527D2575A00766DBE /* CarouselViewController.swift in Sources */,
4E9FC8892790637F00934FFE /* Preferences.swift in Sources */,
Expand Down
13 changes: 7 additions & 6 deletions Zebra/Tabs/Browse/BrowseViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ class BrowseViewController: ListCollectionViewController {
}

private var sources = [Source]()
private var newsItems: [CarouselItem]?
private var newsItems: [CarouselItem]? {
didSet { carouselViewController?.items = newsItems ?? [] }
}

private var dataSource: UICollectionViewDiffableDataSource<Section, Value>!

Expand Down Expand Up @@ -220,18 +222,17 @@ class BrowseViewController: ListCollectionViewController {
}

private func fetchNews() {
Task(priority: .medium) {
Task.detached(priority: .medium) {
do {
if let cachedNews = RedditNewsFetcher.getCached() {
newsItems = cachedNews
await MainActor.run {
self.carouselViewController?.items = cachedNews
self.newsItems = cachedNews
}
}

newsItems = try await RedditNewsFetcher.fetch()
let newsItems = try await RedditNewsFetcher.fetch()
await MainActor.run {
self.carouselViewController?.items = newsItems!
self.newsItems = newsItems
}
} catch {
Logger().warning("Loading news failed: \(String(describing: error))")
Expand Down
14 changes: 9 additions & 5 deletions Zebra/Tabs/Browse/SourceCollectionViewCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ class SourceCollectionViewCell: UICollectionViewCell {
titleLabel.text = source.origin
titleLabel.textColor = .label

updateProgress()
let state = SourceRefreshController.shared.sourceStates[source.uuid]
updateProgress(state: state)
} else {
titleLabel.text = .localize("All Packages")
detailLabel.text = .localize("Browse packages from all sources.")
Expand All @@ -153,9 +154,8 @@ class SourceCollectionViewCell: UICollectionViewCell {
}
}

private func updateProgress() {
if let source = source {
let state = SourceRefreshController.shared.sourceStates[source.uuid]
private func updateProgress(state: SourceRefreshController.SourceState?) {
if source != nil {
if SourceRefreshController.shared.isRefreshing,
let progress = state?.progress {
progressView.isHidden = false
Expand All @@ -180,8 +180,12 @@ class SourceCollectionViewCell: UICollectionViewCell {
}

@objc private func progressDidChange() {
guard let source = source else {
return
}
let state = SourceRefreshController.shared.sourceStates[source.uuid]
DispatchQueue.main.async {
self.updateProgress()
self.updateProgress(state: state)
}
}

Expand Down
5 changes: 1 addition & 4 deletions Zebra/Tabs/Home/Errors/ErrorsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ class ErrorsViewController: ListCollectionViewController {
override class func createLayout() -> UICollectionViewCompositionalLayout {
let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .estimated(44)),
subitems: [
NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .estimated(44)))
])
subitems: [NSCollectionLayoutItem(layoutSize: .full)])
let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [.header]
return UICollectionViewCompositionalLayout(section: section)
Expand Down
10 changes: 5 additions & 5 deletions Zebra/Tabs/Home/HomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import Plains

class HomeViewController: FlowListCollectionViewController {

private var errorCount: UInt = 0

override func viewDidLoad() {
super.viewDidLoad()

Expand Down Expand Up @@ -38,6 +40,9 @@ class HomeViewController: FlowListCollectionViewController {
}

@objc private func refreshProgressDidChange() {
// Filter to only errors. Warnings are mostly annoying and not particularly useful.
errorCount = UInt(SourceRefreshController.shared.refreshErrors.count) + ErrorManager.shared.errorCount(at: .error)

DispatchQueue.main.async {
self.collectionView.reloadData()

Expand All @@ -47,11 +52,6 @@ class HomeViewController: FlowListCollectionViewController {
}
}

private var errorCount: UInt {
// Filter to only errors. Warnings are mostly annoying and not particularly useful.
UInt(SourceRefreshController.shared.refreshErrors.count) + ErrorManager.shared.errorCount(at: .error)
}

@objc private func refreshSources() {
#if !targetEnvironment(macCatalyst)
collectionView.refreshControl!.endRefreshing()
Expand Down
14 changes: 12 additions & 2 deletions Zebra/UI/Carousel/CarouselItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@

import Foundation

struct CarouselItem: Codable {
struct CarouselItem: Codable, Hashable {
internal var uuid = UUID()

let title: String
let subtitle: String?
let url: URL
let url: URL?
let imageURL: URL?

func hash(into hasher: inout Hasher) {
hasher.combine(uuid)
hasher.combine(title)
hasher.combine(subtitle)
hasher.combine(url)
hasher.combine(imageURL)
}
}
96 changes: 34 additions & 62 deletions Zebra/UI/Carousel/CarouselViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import UIKit

class CarouselViewController: UICollectionViewController {
class CarouselViewController: ListCollectionViewController {
static let height: CGFloat = CarouselItemCollectionViewCell.size.height + (15 * 2)

var items = [CarouselItem]() {
Expand All @@ -30,31 +30,32 @@ class CarouselViewController: UICollectionViewController {
internal var activityIndicator: UIActivityIndicatorView!
internal var errorLabel: UILabel!

init() {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CarouselItemCollectionViewCell.size
layout.scrollDirection = .horizontal
layout.minimumInteritemSpacing = 20
layout.sectionInset = UIEdgeInsets(top: 15, left: 20, bottom: 15, right: 20)
super.init(collectionViewLayout: layout)
}
private var dataSource: UICollectionViewDiffableDataSource<Int, CarouselItem>!

@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
override class func createLayout() -> UICollectionViewCompositionalLayout {
let size = CarouselItemCollectionViewCell.size
let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(size.width),
heightDimension: .absolute(size.height)),
subitems: [NSCollectionLayoutItem(layoutSize: .full)])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 20
section.contentInsets = NSDirectionalEdgeInsets(top: 15, leading: 20, bottom: 15, trailing: 20)
section.orthogonalScrollingBehavior = .groupPaging
return UICollectionViewCompositionalLayout(section: section)
}

override func viewDidLoad() {
super.viewDidLoad()

// TODO: Scroll snap/pagination centering on each item
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.alwaysBounceHorizontal = true
collectionView.decelerationRate = .fast
collectionView.backgroundColor = nil
collectionView.register(CarouselItemCollectionViewCell.self, forCellWithReuseIdentifier: "Cell")

dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CarouselItemCollectionViewCell
cell.item = item
return cell
}

activityIndicator = UIActivityIndicatorView(style: .medium)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
activityIndicator.hidesWhenStopped = true
Expand Down Expand Up @@ -86,7 +87,11 @@ class CarouselViewController: UICollectionViewController {
if isError {
isError = false
}
collectionView.reloadData()

var snapshot = NSDiffableDataSourceSnapshot<Int, CarouselItem>()
snapshot.appendSections([0])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: true, completion: nil)
}

collectionView.isUserInteractionEnabled = !isLoading && !isError
Expand All @@ -102,56 +107,21 @@ class CarouselViewController: UICollectionViewController {
isLoading = false
}
}

@objc private func copyItem(_ sender: UICommand) {
let index = sender.propertyList as! Int
let item = items[index]
UIPasteboard.general.string = item.url.absoluteString
}

@objc private func shareItem(_ sender: UICommand) {
let index = sender.propertyList as! Int
guard let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else {
return
}
let item = items[index]
let viewController = UIActivityViewController(activityItems: [item.url], applicationActivities: nil)
viewController.popoverPresentationController?.sourceView = cell
viewController.popoverPresentationController?.sourceRect = cell.bounds
present(viewController, animated: true, completion: nil)
}
}

extension CarouselViewController: UICollectionViewDelegateFlowLayout { // UICollectionViewDataSource, UICollectionViewDelegate
override func numberOfSections(in _: UICollectionView) -> Int {
1
}

override func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
items.count
}

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CarouselItemCollectionViewCell
cell.item = items[indexPath.item]
return cell
}

extension CarouselViewController: UICollectionViewDelegateFlowLayout { // UICollectionViewDelegate
override func collectionView(_: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point _: CGPoint) -> UIContextMenuConfiguration? {
let item = items[indexPath.item]
let cell = collectionView.cellForItem(at: indexPath)!
return UIContextMenuConfiguration(identifier: indexPath as NSCopying, previewProvider: {
// TODO:
// TODO: Preview safari vc?
nil
}, actionProvider: { _ in
UIMenu(children: [
UICommand(title: .copy,
image: UIImage(systemName: "doc.on.doc"),
action: #selector(self.copyItem),
propertyList: indexPath.item),
UICommand(title: .share,
image: UIImage(systemName: "square.and.arrow.up"),
action: #selector(self.shareItem),
propertyList: indexPath.item),
])
.openInBrowser(url: item.url, sender: self),
.copy(text: item.url?.absoluteString),
.share(text: item.title, url: item.url, sender: self, sourceView: cell)
].compact())
})
}

Expand All @@ -161,7 +131,9 @@ extension CarouselViewController: UICollectionViewDelegateFlowLayout { // UIColl

override func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let item = items[indexPath.item]
URLController.open(url: item.url, sender: self, webSchemesOnly: true)
if let url = item.url {
URLController.open(url: url, sender: self, webSchemesOnly: true)
}
}

override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
Expand Down
6 changes: 3 additions & 3 deletions Zebra/UI/Errors/ErrorCollectionViewCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class ErrorCollectionViewCell: UICollectionViewCell {
detailLabel.isEditable = false
detailLabel.backgroundColor = .systemBackground
detailLabel.font = font
detailLabel.textContainerInset = UIEdgeInsets(top: 10, left: 4, bottom: 10, right: 4)
detailLabel.textContainerInset = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
detailLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)

let stackView = UIStackView(arrangedSubviews: [imageView, detailLabel])
Expand All @@ -48,8 +48,8 @@ class ErrorCollectionViewCell: UICollectionViewCell {
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor, constant: 21),
stackView.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -17),
stackView.topAnchor.constraint(equalTo: contentView.topAnchor),
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 6),
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6)
])
}

Expand Down
16 changes: 10 additions & 6 deletions Zebra/UI/List/ListCollectionViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,9 @@ class ListCollectionViewController: BaseListCollectionViewController<UICollectio

extension NSCollectionLayoutGroup {
static func oneAcross(heightDimension: NSCollectionLayoutDimension) -> NSCollectionLayoutGroup {
return NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1),
heightDimension: heightDimension),
subitems: [
NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)))
])
NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1),
heightDimension: heightDimension),
subitems: [NSCollectionLayoutItem(layoutSize: .full)])
}

static func listGrid(environment: NSCollectionLayoutEnvironment, heightDimension: NSCollectionLayoutDimension) -> NSCollectionLayoutGroup {
Expand All @@ -44,3 +41,10 @@ extension NSCollectionLayoutGroup {
count: Int(1 / fraction)))
}
}

extension NSCollectionLayoutSize {
static var full: NSCollectionLayoutSize {
.init(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1))
}
}
Loading

0 comments on commit 557dd17

Please sign in to comment.