一直觉得自己写的不是技术,而是情怀,一个个的教程是自己这一路走来的痕迹。靠专业技能的成功是最具可复制性的,希望我的这条路能让你们少走弯路,希望我能帮你们抹去知识的蒙尘,希望我能帮你们理清知识的脉络,希望未来技术之巅上有你们也有我。
Swift Control Calendar(日历).zip 下载链接
文章目录
效果

使用
bash
import UIKit
import SnapKit
import Alamofire
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupCalendarButton()
}
private func setupCalendarButton() {
let button = UIButton(type: .system)
button.setTitle("选择返程日期", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
button.backgroundColor = .orange
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 12
button.addTarget(self, action: #selector(showCalendar), for: .touchUpInside)
view.addSubview(button)
button.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.equalTo(200)
make.height.equalTo(50)
}
}
@objc private func showCalendar() {
CalendarView.showCalendarView { [weak self] timestamp in
// 处理返回的时间戳
print("选中的时间戳: \(timestamp)")
// 将时间戳转换为日期字符串显示
let date = Date(timeIntervalSince1970: TimeInterval(timestamp))
let formatter = DateFormatter()
formatter.dateFormat = "yyyy年MM月dd日"
let dateString = formatter.string(from: date)
print("选中的日期: \(dateString)")
// 可以在这里更新 UI 或者做其他操作
// 例如:显示一个 Alert 提示
let alert = UIAlertController(
title: "已选择日期",
message: "您选择了 \(dateString)",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "确定", style: .default))
self?.present(alert, animated: true)
}
}
}
封装源码
bash
import UIKit
class CalendarView: UIView {
lazy private var button: UIButton = {
let button = UIButton()
button.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
button.addTarget(self, action: #selector(buttonClick), for: .touchUpInside)
return button
}()
lazy private var backgroundView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 16
view.layer.masksToBounds = true
view.layer.maskedCorners = [
.layerMinXMinYCorner, // 左上
.layerMaxXMinYCorner // 右上
]
view.frame = CGRectMake(0, UIScreen.main.bounds.height, UIScreen.main.bounds.width, 570)
return view
}()
lazy private var confirmButton: UIButton = {
let button = UIButton()
button.setTitle("确认", for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 19, weight: .regular)
button.backgroundColor = UIColor.orange
button.layer.cornerRadius = 24
button.addTarget(self, action: #selector(confirmButtonClick), for: .touchUpInside)
return button
}()
lazy private var titleLabel: UILabel = {
let label = UILabel()
label.text = "请选择返程日期"
label.textColor = .orange
label.font = UIFont.systemFont(ofSize: 21, weight: .bold)
return label
}()
lazy private var dateLabel: UILabel = {
let label = UILabel()
label.text = "2026年3月"
label.textColor = .black
label.font = UIFont.systemFont(ofSize: 19, weight: .medium)
return label
}()
lazy private var previousMonthButton: UIButton = {
let button = UIButton()
if let path = Bundle.main.path(forResource: "date_previous", ofType: "png") {
let image = UIImage(contentsOfFile: path)
button.setImage(image, for: .normal)
}
// button.setImage(UIImage(named: "date_previous"), for: .normal)
button.addTarget(self, action: #selector(previousMonthButtonClick), for: .touchUpInside)
return button
}()
lazy private var nextMonthButton: UIButton = {
let button = UIButton()
if let path = Bundle.main.path(forResource: "date_next", ofType: "png") {
let image = UIImage(contentsOfFile: path)
button.setImage(image, for: .normal)
}
// button.setImage(UIImage(named: "date_next"), for: .normal)
button.addTarget(self, action: #selector(nextMonthButtonClick), for: .touchUpInside)
return button
}()
lazy private var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let width = UIScreen.main.bounds.width / 7.0
layout.itemSize = CGSize(width: width, height: 50)
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
layout.headerReferenceSize = CGSize(width: UIScreen.main.bounds.width, height: 40)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = .white
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(CalendarCell.self, forCellWithReuseIdentifier: "cell")
collectionView.register(CalendarHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header")
return collectionView
}()
private var valueBlock:((_ index:Int)->())?
private var currentDate = Date()
private var currentMonthOffset = 0
private var days: [Int] = []
private var firstWeekday = 0
private var selectedDay: Int?
private var today: Int?// 用来显示今日的位置
private var keyWindow: UIWindow?
static func showCalendarView(valueBlock:((Int)->())?) {
let keyWindow = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.flatMap { $0.windows }.first { $0.isKeyWindow }
guard let keyWindow = keyWindow else { return }
let calendarView = CalendarView()
calendarView.keyWindow = keyWindow
calendarView.valueBlock = valueBlock
keyWindow.addSubview(calendarView)
calendarView.showView()
}
override init(frame: CGRect) {
let targetRect = CGRect(
x: 0,
y: 0,
width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height
)
super.init(frame: targetRect)
buildUI()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func buildUI(){
backgroundColor = .clear
addSubview(button)
button.addSubview(backgroundView)
backgroundView.addSubview(confirmButton)
confirmButton.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.right.equalToSuperview().offset(-15)
make.height.equalTo(48)
make.bottom.equalToSuperview().offset(-(keyWindow?.safeAreaInsets.bottom ?? 0 + 20))
}
backgroundView.addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(15)
make.left.equalToSuperview().offset(15)
}
backgroundView.addSubview(dateLabel)
dateLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(15)
make.left.equalToSuperview().offset(15)
}
backgroundView.addSubview(nextMonthButton)
nextMonthButton.snp.makeConstraints { make in
make.width.height.equalTo(30)
make.right.equalToSuperview().offset(-25)
make.centerY.equalTo(dateLabel)
}
backgroundView.addSubview(previousMonthButton)
previousMonthButton.snp.makeConstraints { make in
make.width.height.equalTo(30)
make.right.equalTo(nextMonthButton.snp.left).offset(-25)
make.centerY.equalTo(dateLabel)
}
backgroundView.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.left.equalToSuperview()
make.right.equalToSuperview()
make.top.equalTo(dateLabel.snp.bottom).offset(20)
make.bottom.equalTo(confirmButton.snp.top).offset(-20)
}
reloadCalendar()
}
func reloadCalendar() {
// 获取日历对象
let calendar = Calendar.current
// 获取当前年月
var dateComponents = calendar.dateComponents([.year, .month], from: Date())
// 加上月份偏移(作用: 点击左右按键控制切换月份)
dateComponents.month! += currentMonthOffset
// 生成目标日期
let date = calendar.date(from: dateComponents)!
// 获取这个月的天数范围 range打印结果: (1..<31) 或者 (1..<30) 或者 (1..<28)
let range = calendar.range(of: .day, in: .month, for: date)!
// 找到"这个月的第一天
let firstDay = calendar.date(from: calendar.dateComponents([.year, .month], from: date))!
// 计算 1 号是星期几(关键)
// 为什么要减 1?因为你的 UI 是: 日(0) 一(1) 二(2) 三(3) 四(4) 五(5) 六(6)
firstWeekday = calendar.component(.weekday, from: firstDay) - 1 // 0~6
// 转成数组,最终得到: [1,2,3,...30]
days = Array(range)
// 默认选中今天
if currentMonthOffset == 0 {
selectedDay = calendar.component(.day, from: Date())
today = calendar.component(.day, from: Date())
} else {
selectedDay = nil
}
// 更新标题
let formatter = DateFormatter()
formatter.dateFormat = "yyyy年M月"
dateLabel.text = formatter.string(from: date)
collectionView.reloadData()
}
func showView(){
UIView.animate(withDuration: 0.5, animations: { [weak self] in
guard let self = self else { return }
self.backgroundColor = .black.withAlphaComponent(0.5)
})
UIView.animate(withDuration: 0.3) { [weak self] in
guard let self = self else { return }
self.backgroundView.frame = CGRectMake(0, UIScreen.main.bounds.height - 570, UIScreen.main.bounds.width, 570)
}
}
func dismissView(){
self.removeFromSuperview()
backgroundColor = .clear
}
@objc func buttonClick() {
self.dismissView()
}
@objc func nextMonthButtonClick() {
currentMonthOffset += 1
reloadCalendar()
}
@objc func previousMonthButtonClick() {
currentMonthOffset -= 1
reloadCalendar()
}
@objc func confirmButtonClick() {
guard let day = selectedDay else { return }
let calendar = Calendar.current
// 1. 计算当前显示的年月(注意带 offset)
var components = calendar.dateComponents([.year, .month], from: Date())
components.month! += currentMonthOffset
components.day = day
components.hour = 0
components.minute = 0
components.second = 0
// 2. 生成 Date
guard let date = calendar.date(from: components) else { return }
// 3. 转时间戳(10位)
let timestamp = Int(date.timeIntervalSince1970)
// 4. 回调
valueBlock?(timestamp)
// 5. 关闭视图
dismissView()
}
}
extension CalendarView: UICollectionViewDelegate, UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return days.count + firstWeekday
}
func collectionView(_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionHeader {
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: "header",
for: indexPath
) as! CalendarHeaderView
return header
}
return UICollectionReusableView()
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CalendarCell
if indexPath.item < firstWeekday {
cell.setEmpty()
} else {
let day = days[indexPath.item - firstWeekday]
cell.setDay(day: day, today: today, currentMonthOffset: currentMonthOffset)
let isPast = isPastDay(day: day)
let isSelected = (day == selectedDay)
cell.updateState(isPast: isPast, isSelected: isSelected)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.item < firstWeekday { return }
let day = days[indexPath.item - firstWeekday]
if isPastDay(day: day) { return }
selectedDay = day
collectionView.reloadData()
}
//判断某一天是否是"过去的日期"(用于置灰、禁止点击等 UI 逻辑)
func isPastDay(day: Int) -> Bool {
let calendar = Calendar.current
if currentMonthOffset < 0 { return true }
if currentMonthOffset > 0 { return false }
let today = calendar.component(.day, from: Date())
return day < today
}
}
class CalendarCell: UICollectionViewCell {
lazy private var label: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 15)
return label
}()
lazy private var subLabel: UILabel = {
let label = UILabel()
label.text = "今天"
label.isHidden = true
label.font = UIFont.systemFont(ofSize: 9, weight: .regular)
label.textColor = .black //.white
return label
}()
lazy private var bgView: UIView = {
let view = UIView()
view.layer.cornerRadius = 8
view.layer.masksToBounds = true
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(bgView)
bgView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(46)
}
bgView.addSubview(label)
label.snp.makeConstraints { make in
make.centerY.equalToSuperview().offset(8)
make.centerX.equalToSuperview()
}
bgView.addSubview(subLabel)
subLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview().offset(-8)
make.centerX.equalToSuperview()
}
}
required init?(coder: NSCoder) { fatalError() }
func setEmpty() {
label.text = ""
bgView.backgroundColor = .clear
}
func setDay(day: Int, today: Int?, currentMonthOffset: Int) {
label.text = "\(day)"
if today == day && currentMonthOffset == 0{
subLabel.isHidden = false
} else {
subLabel.isHidden = true
}
}
func updateState(isPast: Bool, isSelected: Bool) {
if isSelected {// 选中
bgView.backgroundColor = .orange
label.textColor = .white
subLabel.textColor = .white
} else {// 未选中
bgView.backgroundColor = .clear
subLabel.textColor = .black
if isPast { // 过去的日期
label.textColor = UIColor.lightGray
} else {// 未来的日期
label.textColor = UIColor.black
}
}
}
}
class CalendarHeaderView: UICollectionReusableView {
let titles = ["日","一","二","三","四","五","六"]
override init(frame: CGRect) {
super.init(frame: frame)
let width = UIScreen.main.bounds.width / 7.0
for i in 0..<7 {
let label = UILabel(frame: CGRect(x: CGFloat(i) * width, y: 0, width: width, height: 40))
label.text = titles[i]
label.textAlignment = .center
label.textColor = .black
label.font = UIFont.systemFont(ofSize: 19, weight: .bold)
addSubview(label)
}
}
required init?(coder: NSCoder) { fatalError() }
}