-
Notifications
You must be signed in to change notification settings - Fork 124
UI Improvements to the Main Collection View #405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
6bd740e
4304e40
26595b2
4fa2ddf
dd748b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // | ||
| // ChooseGameController+Prefetching.swift | ||
| // MAME4iOS | ||
| // | ||
| // Created by Yoshi Sugawara on 5/31/22. | ||
| // Copyright © 2022 MAME4iOS Team. All rights reserved. | ||
| // | ||
|
|
||
| extension ChooseGameController: UICollectionViewDataSourcePrefetching { | ||
| public func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { | ||
| print("ChooseGameController: Start prefetch of images for indexPaths: \(indexPaths)") | ||
| for indexPath in indexPaths { | ||
| guard let game = getGameInfo(indexPath), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you can just call the gameLoadImage (same thing when setting up cell) and ignore the result |
||
| let imageUrl = game.gameImageURLs.first else { | ||
| continue | ||
| } | ||
| guard ImageCache.sharedInstance().getImage(imageUrl) == nil else { | ||
ToddLa marked this conversation as resolved.
Show resolved
Hide resolved
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add a NS_SWIFT_NAME so it is just ImageCache.shared |
||
| print("image already in cache, not fetching...") | ||
| continue | ||
| } | ||
| ImageCache.sharedInstance().getImage(imageUrl, localURL: game.gameLocalImageURL) { _ in | ||
| print("Prefetch: fetched image and put in cache") | ||
| } | ||
| } | ||
| } | ||
| } | ||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // | ||
| // Collection+Safe.swift | ||
| // MAME4iOS | ||
| // | ||
| // Created by Yoshi Sugawara on 5/31/22. | ||
| // Copyright © 2022 MAME4iOS Team. All rights reserved. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| extension Collection { | ||
| /// Returns the element at the specified index if it is within bounds, otherwise nil. | ||
| subscript (safe index: Index) -> Element? { | ||
| return indices.contains(index) ? self[index] : nil | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -137,7 +137,7 @@ class GameInfoCell : UICollectionViewCell { | |
| override func layoutSubviews() { | ||
| super.layoutSubviews() | ||
|
|
||
| var rect = bounds | ||
| var rect = bounds.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 32)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like the hard coded constants of 30 and 28 should be based on the height of the cell, or line height of the font ALao what happens on tvOS, do we still have these buttons? |
||
|
|
||
| if let size = image.image?.size, size != .zero { | ||
| // use imageAspect unless it is zero | ||
|
|
@@ -243,11 +243,45 @@ class GameInfoCell : UICollectionViewCell { | |
| // MARK: GameInfoHeader - same as GameInfoCell | ||
|
|
||
| class GameInfoHeader : GameInfoCell { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was expecting this header cell to get used like this! |
||
| let expandCollapseButton: UIButton = { | ||
| let button = UIButton(type: .custom) | ||
| button.translatesAutoresizingMaskIntoConstraints = false | ||
| button.contentMode = .scaleAspectFill | ||
| button.contentHorizontalAlignment = .fill | ||
| button.contentVerticalAlignment = .fill | ||
| button.setImage(UIImage(systemName: "minus.square"), for: .normal) | ||
| button.setImage(UIImage(systemName: "plus.square"), for: .selected) | ||
| button.heightAnchor.constraint(equalToConstant: 28).isActive = true | ||
| button.widthAnchor.constraint(equalTo: button.heightAnchor).isActive = true | ||
| return button | ||
| }() | ||
|
|
||
| #if os(tvOS) | ||
| override var canBecomeFocused: Bool { | ||
| return true | ||
| } | ||
| #endif | ||
|
|
||
| var didToggleClosure: (() -> Void)? | ||
|
|
||
| override init(frame: CGRect) { | ||
| super.init(frame: frame) | ||
| contentView.addSubview(expandCollapseButton) | ||
| NSLayoutConstraint.activate([ | ||
| expandCollapseButton.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -8), | ||
| expandCollapseButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) | ||
| ]) | ||
| expandCollapseButton.addTarget(self, action: #selector(expandCollapseButtonPressed(_:)), for: .touchUpInside) | ||
| } | ||
|
|
||
| required init?(coder: NSCoder) { | ||
| fatalError("init(coder:) has not been implemented") | ||
| } | ||
|
|
||
| @objc func expandCollapseButtonPressed(_ sender: UIButton) { | ||
| sender.isSelected.toggle() | ||
| didToggleClosure?() | ||
| } | ||
| } | ||
|
|
||
| // MARK: GameInfoCellLayout - a subclass that will invalidate the layout (for real) on a size change | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| // | ||
| // RecentlyPlayedCell.swift | ||
| // MAME4iOS | ||
| // | ||
| // Created by Yoshi Sugawara on 5/29/22. | ||
| // Copyright © 2022 MAME4iOS Team. All rights reserved. | ||
| // | ||
|
|
||
| import UIKit | ||
|
|
||
| @objcMembers class RecentlyPlayedCell: UICollectionViewCell { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a lot of experience with nested UICollectionView's and I am surprised how well they work, this looks good. I like how you did not duplicate all the cell setup, and just made this a container for "the same cells" from the parent. |
||
| static let identifier = "RecentlyPlayedCell" | ||
|
|
||
| var items = [GameInfo]() { | ||
| didSet { | ||
| collectionView.reloadData() | ||
| } | ||
| } | ||
|
|
||
| let collectionView: UICollectionView = { | ||
| let layout = UICollectionViewFlowLayout() | ||
| layout.scrollDirection = .horizontal | ||
| let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) | ||
| return collectionView | ||
| }() | ||
|
|
||
| var itemSize = CGSize.zero { | ||
| didSet { | ||
| guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { | ||
| return | ||
| } | ||
| layout.itemSize = itemSize | ||
| } | ||
| } | ||
| var setupCellClosure: ((GameInfoCell, IndexPath) -> Void)? | ||
| var selectItemClosure: ((IndexPath) -> Void)? | ||
| var contextMenuClosure: ((IndexPath) -> UIContextMenuConfiguration?)? | ||
| var heightForGameInfoClosure: ((GameInfo, UICollectionViewLayout) -> CGPoint)? | ||
|
|
||
| override init(frame: CGRect) { | ||
| super.init(frame: frame) | ||
| setupViews() | ||
| } | ||
|
|
||
| required init?(coder: NSCoder) { | ||
| fatalError("init(coder:) has not been implemented") | ||
| } | ||
|
|
||
| private func setupViews() { | ||
| backgroundColor = .clear | ||
| contentView.addSubview(collectionView) | ||
| collectionView.translatesAutoresizingMaskIntoConstraints = false | ||
| NSLayoutConstraint.activate([ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is This is really better than a A single line
|
||
| collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), | ||
| collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), | ||
| collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), | ||
| collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) | ||
| ]) | ||
| collectionView.dataSource = self | ||
| collectionView.delegate = self | ||
| collectionView.register(GameInfoCell.self, forCellWithReuseIdentifier: Self.identifier) | ||
| } | ||
| } | ||
|
|
||
| extension RecentlyPlayedCell: UICollectionViewDataSource { | ||
| func numberOfSections(in collectionView: UICollectionView) -> Int { | ||
| return 1 | ||
| } | ||
|
|
||
| func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { | ||
| return items.count | ||
| } | ||
|
|
||
| func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { | ||
| guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Self.identifier, for: indexPath) as? GameInfoCell else { | ||
| return UICollectionViewCell() | ||
| } | ||
| setupCellClosure?(cell, indexPath) | ||
| return cell | ||
| } | ||
| } | ||
|
|
||
| extension RecentlyPlayedCell: UICollectionViewDelegate { | ||
| func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { | ||
| selectItemClosure?(indexPath) | ||
| } | ||
|
|
||
| func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { | ||
| return contextMenuClosure?(indexPath) | ||
| } | ||
| } | ||
|
|
||
| extension RecentlyPlayedCell: UICollectionViewDelegateFlowLayout { | ||
| func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { | ||
| guard let layout = collectionViewLayout as? UICollectionViewFlowLayout, | ||
| let heightForGameInfoClosure = heightForGameInfoClosure, | ||
| let game = items[safe: indexPath.row] else { | ||
| return .zero | ||
| } | ||
| let heightInfo = heightForGameInfoClosure(game, layout) | ||
| let height = heightInfo.x + heightInfo.y | ||
| return CGSize(width: layout.itemSize.width, height: height) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am worried that iOS is gonna go crazy pre-loading and we will fire off a lot of net requests, we can't cancel