Swift Control Calendar(日历)

一直觉得自己写的不是技术,而是情怀,一个个的教程是自己这一路走来的痕迹。靠专业技能的成功是最具可复制性的,希望我的这条路能让你们少走弯路,希望我能帮你们抹去知识的蒙尘,希望我能帮你们理清知识的脉络,希望未来技术之巅上有你们也有我。

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() }
}
相关推荐
今天AI了吗2 小时前
Python 基础语法(一):常量、变量、输入输出与运算符
开发语言·数据库·人工智能·python·sql·深度学习·机器学习
TELL5212 小时前
selenium webdriver 第二次初始化的异常
开发语言·python
动词ing3 小时前
【C语言】自定义函数+指针入门
c语言·开发语言·算法
重生之后端学习3 小时前
283. 移动零[简单]✅
开发语言·数据结构·算法·leetcode·职场和发展
动词ing4 小时前
【C语言】结构体+文件基础
c语言·开发语言·数据结构
caimouse4 小时前
ReactOS 窗口系统分析(7):分层窗口与绘制辅助 — layered.c + draw.c
c语言·开发语言
丰锋ff5 小时前
基于 Qt 的智慧社区物业管理系统
开发语言·qt
夜不会漫长5 小时前
C++入门(1)
开发语言·c++·算法
大鹏说大话5 小时前
从爬虫到决策引擎:大数据下自媒体如何用Python挖掘用户痛点
开发语言·爬虫·python
Niuguangshuo5 小时前
silero-vad:超轻量级开源 VAD 实践指南
开发语言·python