|
|
@@ -17,43 +17,57 @@ protocol LNGameCategoryListViewDelegate: NSObject {
|
|
|
|
|
|
class LNGameCategoryListView: UIView {
|
|
|
private let columns = 3
|
|
|
- private let scrollView = UIScrollView()
|
|
|
- private let stackView = UIStackView()
|
|
|
- private var categoryViews: [UIView] = []
|
|
|
+ private let collectionViewLayout = UICollectionViewFlowLayout()
|
|
|
+ private let collectionView: UICollectionView
|
|
|
|
|
|
- private var curCategories: [LNGameTypeItemVO] = []
|
|
|
+ private var categories: [LNGameTypeItemVO] = []
|
|
|
|
|
|
weak var delegate: LNGameCategoryListViewDelegate?
|
|
|
|
|
|
override init(frame: CGRect) {
|
|
|
+ collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
|
|
super.init(frame: frame)
|
|
|
|
|
|
setupViews()
|
|
|
}
|
|
|
|
|
|
func update(categories: [LNGameTypeItemVO]) {
|
|
|
- let old = stackView.arrangedSubviews
|
|
|
- old.forEach {
|
|
|
- stackView.removeArrangedSubview($0)
|
|
|
- $0.removeFromSuperview()
|
|
|
- }
|
|
|
+ self.categories = categories
|
|
|
+ collectionView.reloadData()
|
|
|
|
|
|
- categoryViews.removeAll()
|
|
|
- for category in categories {
|
|
|
- let view = buildCategoryPanel(category: category)
|
|
|
- categoryViews.append(view)
|
|
|
- stackView.addArrangedSubview(view)
|
|
|
+ DispatchQueue.main.async { [weak self] in
|
|
|
+ guard let self else { return }
|
|
|
+ fixBottomSpace()
|
|
|
}
|
|
|
- curCategories = categories
|
|
|
}
|
|
|
|
|
|
func scrollTo(category: LNGameTypeItemVO) {
|
|
|
- guard let index = curCategories.firstIndex(where: { $0.code == category.code }),
|
|
|
- index < categoryViews.count else {
|
|
|
+ guard let index = categories.firstIndex(where: { $0.code == category.code }) else {
|
|
|
return
|
|
|
}
|
|
|
- let view = categoryViews[index]
|
|
|
- scrollView.scrollRectToVisible(view.frame, animated: true)
|
|
|
+ DispatchQueue.main.async { [weak self] in
|
|
|
+ guard let self else { return }
|
|
|
+ if let headerAttributes = collectionView.layoutAttributesForSupplementaryElement(
|
|
|
+ ofKind: UICollectionView.elementKindSectionHeader,
|
|
|
+ at: IndexPath(item: 0, section: index))
|
|
|
+ {
|
|
|
+ collectionView.setContentOffset(.init(x: 0, y: headerAttributes.frame.origin.y), animated: true)
|
|
|
+ } else {
|
|
|
+ collectionView.scrollToItem(at: .init(row: 0, section: index), at: .top, animated: true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override func layoutSubviews() {
|
|
|
+ super.layoutSubviews()
|
|
|
+
|
|
|
+ let width = (bounds.width - collectionViewLayout.minimumInteritemSpacing) / CGFloat(columns) - collectionViewLayout.minimumInteritemSpacing
|
|
|
+ collectionViewLayout.itemSize = .init(width: width, height: 68)
|
|
|
+
|
|
|
+ DispatchQueue.main.async { [weak self] in
|
|
|
+ guard let self else { return }
|
|
|
+ fixBottomSpace()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
@@ -62,100 +76,143 @@ class LNGameCategoryListView: UIView {
|
|
|
}
|
|
|
|
|
|
extension LNGameCategoryListView: UIScrollViewDelegate {
|
|
|
- func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
|
- checkPosition(scrollView)
|
|
|
- }
|
|
|
-
|
|
|
- private func checkPosition(_ scrollView: UIScrollView) {
|
|
|
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
let offsetY = scrollView.contentOffset.y
|
|
|
|
|
|
- let index = categoryViews.firstIndex {
|
|
|
- CGRectGetMinY($0.frame) > offsetY
|
|
|
+ var section = 0
|
|
|
+ for index in 0..<categories.count {
|
|
|
+ if let headerAttributes = collectionView.layoutAttributesForSupplementaryElement(
|
|
|
+ ofKind: UICollectionView.elementKindSectionHeader,
|
|
|
+ at: IndexPath(item: 0, section: index)) {
|
|
|
+ if offsetY >= headerAttributes.frame.origin.y {
|
|
|
+ section = index
|
|
|
+ } else {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- let fixedIndex = if let index, index > 0 {
|
|
|
- index - 1
|
|
|
- } else {
|
|
|
- 0
|
|
|
+ delegate?.onCategoryListView(view: self, didScrollTo: categories[section])
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+extension LNGameCategoryListView: UICollectionViewDataSource, UICollectionViewDelegate {
|
|
|
+ func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
|
+ categories[section].children.count
|
|
|
+ }
|
|
|
+
|
|
|
+ func numberOfSections(in collectionView: UICollectionView) -> Int {
|
|
|
+ categories.count
|
|
|
+ }
|
|
|
+
|
|
|
+ func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
|
+ let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LNGameCategoryListCell.className, for: indexPath) as! LNGameCategoryListCell
|
|
|
+
|
|
|
+ let item = categories[indexPath.section].children[indexPath.row]
|
|
|
+ cell.tabItemView.update(item)
|
|
|
+
|
|
|
+ return cell
|
|
|
+ }
|
|
|
+
|
|
|
+ func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
|
|
|
+ guard kind == UICollectionView.elementKindSectionHeader else {
|
|
|
+ return UICollectionReusableView()
|
|
|
}
|
|
|
|
|
|
- delegate?.onCategoryListView(view: self, didScrollTo: curCategories[fixedIndex])
|
|
|
+ let view = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: LNGameCategoryListHeader.className, for: indexPath) as! LNGameCategoryListHeader
|
|
|
+
|
|
|
+ let section = categories[indexPath.section]
|
|
|
+ view.update(section.name)
|
|
|
+
|
|
|
+ return view
|
|
|
+ }
|
|
|
+
|
|
|
+ func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
|
+ let category = categories[indexPath.section]
|
|
|
+ let game = category.children[indexPath.row]
|
|
|
+ pushToGameMateList(topCategory: category, category: game, filter: LNGameMateFilter())
|
|
|
}
|
|
|
}
|
|
|
|
|
|
extension LNGameCategoryListView {
|
|
|
- private func setupViews() {
|
|
|
- scrollView.delegate = self
|
|
|
- scrollView.showsVerticalScrollIndicator = false
|
|
|
- scrollView.showsHorizontalScrollIndicator = false
|
|
|
- addSubview(scrollView)
|
|
|
- scrollView.snp.makeConstraints { make in
|
|
|
- make.directionalEdges.equalToSuperview()
|
|
|
+ private func fixBottomSpace() {
|
|
|
+ guard !categories.isEmpty else { return }
|
|
|
+ if let headerAttributes = collectionView.layoutAttributesForSupplementaryElement(
|
|
|
+ ofKind: UICollectionView.elementKindSectionHeader,
|
|
|
+ at: IndexPath(item: 0, section: categories.count - 1)) {
|
|
|
+ let offset = collectionView.contentSize.height - headerAttributes.frame.origin.y
|
|
|
+ collectionView.contentInset = .init(top: 0, left: 0, bottom: bounds.height - offset, right: 0)
|
|
|
}
|
|
|
+ }
|
|
|
+
|
|
|
+ private func setupViews() {
|
|
|
+ collectionViewLayout.itemSize = .init(width: 77, height: 68)
|
|
|
+ collectionViewLayout.minimumLineSpacing = 10
|
|
|
+ collectionViewLayout.minimumInteritemSpacing = 12
|
|
|
+ collectionViewLayout.headerReferenceSize = .init(width: 100, height: LNGameCategoryListHeader.height)
|
|
|
+ collectionViewLayout.sectionInset = .init(top: 0, left: 12, bottom: 40, right: 12)
|
|
|
|
|
|
- let fakeView = UIView()
|
|
|
- scrollView.addSubview(fakeView)
|
|
|
- fakeView.snp.makeConstraints { make in
|
|
|
- make.leading.top.trailing.equalToSuperview()
|
|
|
- make.width.equalToSuperview()
|
|
|
- make.height.equalTo(0)
|
|
|
+ collectionView.backgroundColor = .clear
|
|
|
+ collectionView.dataSource = self
|
|
|
+ collectionView.delegate = self
|
|
|
+ collectionView.contentInsetAdjustmentBehavior = .never
|
|
|
+ collectionView.showsHorizontalScrollIndicator = false
|
|
|
+ collectionView.showsVerticalScrollIndicator = false
|
|
|
+ collectionView.register(LNGameCategoryListCell.self, forCellWithReuseIdentifier: LNGameCategoryListCell.className)
|
|
|
+ collectionView.register(LNGameCategoryListHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: LNGameCategoryListHeader.className)
|
|
|
+ addSubview(collectionView)
|
|
|
+ collectionView.snp.makeConstraints { make in
|
|
|
+ make.directionalEdges.equalToSuperview()
|
|
|
}
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private class LNGameCategoryListCell: UICollectionViewCell {
|
|
|
+ let tabItemView = LNHomeGameTabItemView()
|
|
|
+ override init(frame: CGRect) {
|
|
|
+ super.init(frame: frame)
|
|
|
|
|
|
- stackView.axis = .vertical
|
|
|
- stackView.spacing = 26
|
|
|
- scrollView.addSubview(stackView)
|
|
|
- stackView.snp.makeConstraints { make in
|
|
|
- make.leading.equalToSuperview().offset(10)
|
|
|
- make.trailing.equalToSuperview().offset(-10)
|
|
|
- make.top.equalToSuperview().offset(18)
|
|
|
- make.bottom.equalToSuperview()
|
|
|
+ contentView.addSubview(tabItemView)
|
|
|
+ tabItemView.snp.makeConstraints { make in
|
|
|
+ make.directionalEdges.equalToSuperview()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private func buildCategoryPanel(category: LNGameTypeItemVO) -> UIView {
|
|
|
- let container = UIStackView()
|
|
|
- container.axis = .vertical
|
|
|
- container.spacing = 10
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private class LNGameCategoryListHeader: UICollectionReusableView {
|
|
|
+ static let height = 36
|
|
|
+ private let titleLabel = UILabel()
|
|
|
+
|
|
|
+ override init(frame: CGRect) {
|
|
|
+ super.init(frame: frame)
|
|
|
|
|
|
- let titleLabel = UILabel()
|
|
|
- titleLabel.text = category.name
|
|
|
- titleLabel.font = .heading_h3
|
|
|
- titleLabel.textColor = .text_5
|
|
|
- container.addArrangedSubview(titleLabel)
|
|
|
-
|
|
|
- var stackView = buildLineStackView()
|
|
|
- container.addArrangedSubview(stackView)
|
|
|
- for game in category.children {
|
|
|
- let itemView = LNHomeGameTabItemView()
|
|
|
- itemView.update(game)
|
|
|
- itemView.onTap { [weak self] in
|
|
|
- guard let self else { return }
|
|
|
- pushToGameMateList(topCategory: category, category: game, filter: LNGameMateFilter())
|
|
|
- }
|
|
|
- stackView.addArrangedSubview(itemView)
|
|
|
- if stackView.arrangedSubviews.count == columns {
|
|
|
- stackView = buildLineStackView()
|
|
|
- container.addArrangedSubview(stackView)
|
|
|
- }
|
|
|
- }
|
|
|
- if stackView.arrangedSubviews.count < columns {
|
|
|
- for _ in stackView.arrangedSubviews.count..<columns {
|
|
|
- stackView.addArrangedSubview(UIView())
|
|
|
- }
|
|
|
+ snp.makeConstraints { make in
|
|
|
+ make.height.equalTo(Self.height)
|
|
|
}
|
|
|
|
|
|
- return container
|
|
|
+ titleLabel.font = .heading_h2
|
|
|
+ titleLabel.textColor = .text_5
|
|
|
+ addSubview(titleLabel)
|
|
|
+ titleLabel.snp.makeConstraints { make in
|
|
|
+ make.bottom.equalToSuperview().offset(-8)
|
|
|
+ make.leading.equalToSuperview().offset(10)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- private func buildLineStackView() -> UIStackView {
|
|
|
- let stackView = UIStackView()
|
|
|
- stackView.axis = .horizontal
|
|
|
- stackView.distribution = .fillEqually
|
|
|
- stackView.spacing = 15
|
|
|
- return stackView
|
|
|
+ func update(_ title: String) {
|
|
|
+ titleLabel.text = title
|
|
|
+ }
|
|
|
+
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+
|
|
|
#if DEBUG
|
|
|
|
|
|
import SwiftUI
|