Instantly share code, notes, and snippets.
Last active
September 7, 2021 09:54
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save Jimmy-Prime/72d6ff5ff964079562a6bd9a777d9c8e to your computer and use it in GitHub Desktop.
POC implementation, using UIListContentConfiguration with UICollectionView to define UI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
protocol DropdownMenuItem { | |
var appearance: UIListContentConfiguration { get } | |
} | |
protocol DropdownMenuSection { | |
associatedtype Item: DropdownMenuItem | |
var appearance: UIListContentConfiguration? { get } | |
var items: [Item] { get } | |
} | |
class NavigationDropdownMenu<Section: DropdownMenuSection>: NSObject, UICollectionViewDataSource, UICollectionViewDelegate { | |
var sections: [Section] = [] { | |
didSet { | |
collectionView.reloadData() | |
} | |
} | |
var isShowing: Bool { collectionView.superview != nil } | |
private var collectionView: UICollectionView! | |
private var cellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, UIListContentConfiguration>! | |
override init() { | |
super.init() | |
generateMenu() | |
} | |
private func generateMenu() { | |
let layoutConfig = UICollectionLayoutListConfiguration(appearance: .plain) | |
let layout = UICollectionViewCompositionalLayout.list(using: layoutConfig) | |
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) | |
collectionView.dataSource = self | |
collectionView.delegate = self | |
cellRegistration = .init { cell, indexPath, itemIdentifier in | |
cell.contentConfiguration = itemIdentifier | |
} | |
} | |
func show(in vc: UIViewController, animated: Bool = true) { | |
vc.view.addSubview(collectionView) | |
collectionView.translatesAutoresizingMaskIntoConstraints = false | |
let fixHeightConstraint = collectionView.heightAnchor.constraint(equalToConstant: 10) | |
fixHeightConstraint.priority = .defaultLow | |
let heightRatioConstraint = collectionView.heightAnchor.constraint(lessThanOrEqualTo: vc.view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.6) | |
heightRatioConstraint.priority = .required | |
NSLayoutConstraint.activate([ | |
collectionView.topAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.topAnchor), | |
collectionView.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor), | |
collectionView.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor), | |
fixHeightConstraint, | |
heightRatioConstraint, | |
]) | |
DispatchQueue.main.async { [weak self] in | |
// now collectionView is laid out | |
if let height = self?.collectionView.collectionViewLayout.collectionViewContentSize.height { | |
fixHeightConstraint.constant = height | |
} | |
if animated { | |
self?.collectionView.transform = .init(translationX: 0, y: -20) | |
self?.collectionView.alpha = 0.01 // has to be non 0 | |
UIView.animate(withDuration: 0.3) { | |
self?.collectionView.transform = .identity | |
self?.collectionView.alpha = 1 | |
} | |
} | |
} | |
} | |
func hide(animated: Bool = true) { | |
if !animated { | |
collectionView.removeFromSuperview() | |
} else { | |
UIView.animate(withDuration: 0.3) { [weak self] in | |
self?.collectionView.alpha = 0 | |
self?.collectionView.transform = .init(translationX: 0, y: -20) | |
} completion: { [weak self] _ in | |
self?.collectionView.removeFromSuperview() | |
self?.collectionView.alpha = 1 | |
self?.collectionView.transform = .identity | |
} | |
} | |
} | |
// MARK: - UICollectionViewDataSource | |
func numberOfSections(in collectionView: UICollectionView) -> Int { | |
return sections.count | |
} | |
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { | |
let itemsCount = sections[section].items.count | |
return sections[section].appearance == nil ? itemsCount : itemsCount + 1 | |
} | |
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { | |
let item: UIListContentConfiguration | |
if let header = sections[indexPath.section].appearance { | |
if indexPath.item == 0 { | |
item = header | |
} else { | |
item = sections[indexPath.section].items[indexPath.item - 1].appearance | |
} | |
} else { | |
item = sections[indexPath.section].items[indexPath.item].appearance | |
} | |
let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) | |
if indexPath.item == 0, sections[indexPath.section].appearance != nil { | |
cell.backgroundConfiguration = .clear() | |
} | |
return cell | |
} | |
// MARK: - UICollectionViewDelegate | |
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { | |
if indexPath.item == 0, sections[indexPath.section].appearance != nil { | |
return false | |
} else { | |
return true | |
} | |
} | |
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { | |
let selectedItem: Section.Item | |
if sections[indexPath.section].appearance != nil { | |
selectedItem = sections[indexPath.section].items[indexPath.item - 1] | |
} else { | |
selectedItem = sections[indexPath.section].items[indexPath.item] | |
} | |
print(selectedItem) | |
hide() | |
collectionView.deselectItem(at: indexPath, animated: false) | |
} | |
} | |
struct TestSection: DropdownMenuSection { | |
let index: Int | |
var appearance: UIListContentConfiguration? { | |
var configuration = UIListContentConfiguration.cell() | |
configuration.image = UIImage(systemName: "heart") | |
configuration.text = "Header \(index)" | |
configuration.secondaryText = "Secondary Header Text" | |
return configuration | |
} | |
var items: [TestItem] | |
} | |
struct TestItem: DropdownMenuItem { | |
let index: Int | |
var appearance: UIListContentConfiguration { | |
var configuration = UIListContentConfiguration.cell() | |
configuration.text = "Item \(index)" | |
return configuration | |
} | |
} | |
class ViewController: UIViewController { | |
var menu: NavigationDropdownMenu<TestSection>! | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
view.backgroundColor = .systemBackground | |
let button = UIButton(type: .system) | |
button.setTitle("BUTTON", for: .normal) | |
button.addTarget(self, action: #selector(didTapButton), for: .primaryActionTriggered) | |
navigationItem.titleView = button | |
menu = .init() | |
menu.sections = [ | |
TestSection( | |
index: 0, | |
items: [ | |
.init(index: 1), | |
] | |
), | |
TestSection( | |
index: 1, | |
items: [ | |
.init(index: 0), | |
.init(index: 1), | |
.init(index: 3), | |
.init(index: 5), | |
.init(index: 7), | |
] | |
) | |
] | |
} | |
@objc private func didTapButton() { | |
if menu.isShowing { | |
menu.hide() | |
} else { | |
menu.show(in: self) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment