Files
suixinkan_uikit/suixinkan/UI/Statistics/StatisticsDateRangeViewController.swift

304 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// StatisticsDateRangeViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// Sheet Android `DateRangeBottomSheet`
final class StatisticsDateRangeViewController: UIViewController {
var onShortcutSelected: ((String) -> Void)?
var onRangeSelected: ((Date, Date) -> Void)?
private var currentMonth: Date
private var tempStart: Date?
private var tempEnd: Date?
private var selectedShortcut: String?
private let shortcutStack = UIStackView()
private let monthTitleLabel = UILabel()
private let weekdayStack = UIStackView()
private let daysCollectionView: UICollectionView
private var shortcutButtons: [StatisticsPeriodButton] = []
private let calendar: Calendar = {
var calendar = Calendar(identifier: .gregorian)
calendar.locale = Locale(identifier: "zh_CN")
calendar.firstWeekday = 2
return calendar
}()
init(rangeStart: Date?, rangeEnd: Date?) {
self.tempStart = rangeStart
self.tempEnd = rangeEnd
self.currentMonth = rangeStart ?? Date()
let layout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 8
daysCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
let today = Calendar.current.startOfDay(for: Date())
let initCalendar = Calendar(identifier: .gregorian)
if rangeStart == nil, rangeEnd == nil {
selectedShortcut = StatisticsRangeShortcut.all.rawValue
} else if rangeStart == today, rangeEnd == today {
selectedShortcut = StatisticsRangeShortcut.today.rawValue
} else if
let start = rangeStart,
let end = rangeEnd,
start == initCalendar.date(byAdding: .day, value: -1, to: today),
end == initCalendar.date(byAdding: .day, value: -1, to: today) {
selectedShortcut = StatisticsRangeShortcut.yesterday.rawValue
} else if
let start = rangeStart,
let end = rangeEnd,
start == initCalendar.date(byAdding: .day, value: -6, to: today),
end == today {
selectedShortcut = StatisticsRangeShortcut.last7Days.rawValue
}
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupUI()
updateMonthTitle()
}
private func setupUI() {
shortcutStack.axis = .horizontal
shortcutStack.distribution = .equalSpacing
StatisticsRangeShortcut.allCases.forEach { shortcut in
let button = StatisticsPeriodButton()
button.periodTitle = shortcut.rawValue
button.isSelected = shortcut.rawValue == selectedShortcut
button.addTarget(self, action: #selector(shortcutTapped(_:)), for: .touchUpInside)
shortcutButtons.append(button)
shortcutStack.addArrangedSubview(button)
}
let previousButton = UIButton(type: .system)
previousButton.setImage(UIImage(systemName: "chevron.left"), for: .normal)
previousButton.tintColor = UIColor(hex: 0x9CA3AF)
previousButton.addTarget(self, action: #selector(previousMonth), for: .touchUpInside)
let nextButton = UIButton(type: .system)
nextButton.setImage(UIImage(systemName: "chevron.right"), for: .normal)
nextButton.tintColor = UIColor(hex: 0x9CA3AF)
nextButton.addTarget(self, action: #selector(nextMonth), for: .touchUpInside)
monthTitleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
monthTitleLabel.textAlignment = .center
let monthRow = UIStackView(arrangedSubviews: [previousButton, monthTitleLabel, nextButton])
monthRow.axis = .horizontal
monthRow.alignment = .center
monthRow.distribution = .equalCentering
weekdayStack.axis = .horizontal
weekdayStack.distribution = .fillEqually
["", "", "", "", "", "", ""].forEach { title in
let label = UILabel()
label.text = title
label.font = .systemFont(ofSize: 12)
label.textColor = UIColor(hex: 0x9CA3AF)
label.textAlignment = .center
weekdayStack.addArrangedSubview(label)
}
daysCollectionView.backgroundColor = .clear
daysCollectionView.dataSource = self
daysCollectionView.delegate = self
daysCollectionView.register(DateCell.self, forCellWithReuseIdentifier: DateCell.reuseIdentifier)
view.addSubview(shortcutStack)
view.addSubview(monthRow)
view.addSubview(weekdayStack)
view.addSubview(daysCollectionView)
shortcutStack.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(12)
make.leading.trailing.equalToSuperview().inset(16)
}
monthRow.snp.makeConstraints { make in
make.top.equalTo(shortcutStack.snp.bottom).offset(12)
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(32)
}
previousButton.snp.makeConstraints { make in
make.size.equalTo(24)
}
nextButton.snp.makeConstraints { make in
make.size.equalTo(24)
}
weekdayStack.snp.makeConstraints { make in
make.top.equalTo(monthRow.snp.bottom).offset(8)
make.leading.trailing.equalToSuperview().inset(16)
}
daysCollectionView.snp.makeConstraints { make in
make.top.equalTo(weekdayStack.snp.bottom).offset(8)
make.leading.trailing.equalToSuperview().inset(16)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
make.height.equalTo(240)
}
}
@objc private func shortcutTapped(_ sender: StatisticsPeriodButton) {
onShortcutSelected?(sender.periodTitle)
}
@objc private func previousMonth() {
currentMonth = calendar.date(byAdding: .month, value: -1, to: currentMonth) ?? currentMonth
updateMonthTitle()
daysCollectionView.reloadData()
}
@objc private func nextMonth() {
currentMonth = calendar.date(byAdding: .month, value: 1, to: currentMonth) ?? currentMonth
updateMonthTitle()
daysCollectionView.reloadData()
}
private func updateMonthTitle() {
let components = calendar.dateComponents([.year, .month], from: currentMonth)
monthTitleLabel.text = "\(components.year ?? 0)\(components.month ?? 0)"
}
private func monthDates() -> [Date?] {
guard
let monthStart = calendar.date(from: calendar.dateComponents([.year, .month], from: currentMonth)),
let dayRange = calendar.range(of: .day, in: .month, for: monthStart)
else {
return []
}
let firstWeekday = calendar.component(.weekday, from: monthStart)
let leadingEmpty = (firstWeekday + 5) % 7
var dates: [Date?] = Array(repeating: nil, count: leadingEmpty)
for day in dayRange {
if let date = calendar.date(byAdding: .day, value: day - 1, to: monthStart) {
dates.append(date)
}
}
while dates.count % 7 != 0 {
dates.append(nil)
}
return dates
}
private func handleDateTap(_ date: Date) {
selectedShortcut = nil
shortcutButtons.forEach { $0.isSelected = false }
if tempStart == nil || (tempStart != nil && tempEnd != nil) {
tempStart = date
tempEnd = nil
} else if let start = tempStart {
if date < start {
tempEnd = start
tempStart = date
} else {
tempEnd = date
}
if let start = tempStart, let end = tempEnd {
onRangeSelected?(start, end)
}
}
daysCollectionView.reloadData()
}
private func isInRange(_ date: Date) -> Bool {
guard let start = tempStart else { return false }
if let end = tempEnd {
return date >= min(start, end) && date <= max(start, end)
}
return calendar.isDate(date, inSameDayAs: start)
}
private final class DateCell: UICollectionViewCell {
static let reuseIdentifier = "StatisticsDateCell"
private let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
label.textAlignment = .center
label.font = .systemFont(ofSize: 14)
contentView.addSubview(label)
label.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
contentView.layer.cornerRadius = 16
contentView.clipsToBounds = true
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(text: String?, selected: Bool, inRange: Bool) {
label.text = text
if selected {
contentView.backgroundColor = AppColor.primary
label.textColor = .white
} else if inRange {
contentView.backgroundColor = UIColor(hex: 0xEFF6FF)
label.textColor = AppColor.primary
} else {
contentView.backgroundColor = .clear
label.textColor = .black
}
}
}
}
extension StatisticsDateRangeViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
monthDates().count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: DateCell.reuseIdentifier,
for: indexPath
) as! DateCell
let dates = monthDates()
guard indexPath.item < dates.count, let date = dates[indexPath.item] else {
cell.configure(text: nil, selected: false, inRange: false)
return cell
}
let day = calendar.component(.day, from: date)
let selected = (tempStart != nil && calendar.isDate(date, inSameDayAs: tempStart!))
|| (tempEnd != nil && calendar.isDate(date, inSameDayAs: tempEnd!))
cell.configure(text: "\(day)", selected: selected, inRange: isInRange(date))
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let dates = monthDates()
guard indexPath.item < dates.count, let date = dates[indexPath.item] else { return }
handleDateTap(date)
}
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {
let width = collectionView.bounds.width / 7
return CGSize(width: width, height: 32)
}
}