Skip to content

Instantly share code, notes, and snippets.

@Jimmy-Prime
Last active September 7, 2021 09:54
Show Gist options
  • Save Jimmy-Prime/72d6ff5ff964079562a6bd9a777d9c8e to your computer and use it in GitHub Desktop.
Save Jimmy-Prime/72d6ff5ff964079562a6bd9a777d9c8e to your computer and use it in GitHub Desktop.
POC implementation, using UIListContentConfiguration with UICollectionView to define UI
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