美文网首页
UIKit框架(四十八) —— 自定义Calendar Cont

UIKit框架(四十八) —— 自定义Calendar Cont

作者: 刀客传奇 | 来源:发表于2020-08-26 18:36 被阅读0次

    版本记录

    版本号 时间
    V1.0 2020.08.26 星期三

    前言

    iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
    1. UIKit框架(一) —— UIKit动力学和移动效果(一)
    2. UIKit框架(二) —— UIKit动力学和移动效果(二)
    3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
    4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
    5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
    6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
    7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
    8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
    9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
    10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
    11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
    12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
    13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
    14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
    15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
    16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
    17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
    18. UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)
    19. UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)
    20. UIKit框架(二十) —— 基于UILabel跑马灯类似效果的实现(一)
    21. UIKit框架(二十一) —— UIStackView的使用(一)
    22. UIKit框架(二十二) —— 基于UIPresentationController的自定义viewController的转场和展示(一)
    23. UIKit框架(二十三) —— 基于UIPresentationController的自定义viewController的转场和展示(二)
    24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (一)
    25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (二)
    26. UIKit框架(二十六) —— UICollectionView的自定义布局 (一)
    27. UIKit框架(二十七) —— UICollectionView的自定义布局 (二)
    28. UIKit框架(二十八) —— 一个UISplitViewController的简单实用示例 (一)
    29. UIKit框架(二十九) —— 一个UISplitViewController的简单实用示例 (二)
    30. UIKit框架(三十) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(一)
    31. UIKit框架(三十一) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(二)
    32. UIKit框架(三十二) —— 替换Peek and Pop交互的基于iOS13的Context Menus(一)
    33. UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)
    34. UIKit框架(三十四) —— Accessibility的使用(一)
    35. UIKit框架(三十五) —— Accessibility的使用(二)
    36. UIKit框架(三十六) —— UICollectionView UICollectionViewDiffableDataSource的使用(一)
    37. UIKit框架(三十七) —— UICollectionView UICollectionViewDiffableDataSource的使用(二)
    38. UIKit框架(三十八) —— 基于CollectionView转盘效果的实现(一)
    39. UIKit框架(三十九) —— iOS 13中UISearchController 和 UISearchBar的新更改(一)
    40. UIKit框架(四十) —— iOS 13中UISearchController 和 UISearchBar的新更改(二)
    41. UIKit框架(四十一) —— 使用协议构建自定义Collection(一)
    42. UIKit框架(四十二) —— 使用协议构建自定义Collection(二)
    43. UIKit框架(四十三) —— CALayer的简单实用示例(一)
    44. UIKit框架(四十四) —— CALayer的简单实用示例(二)
    45. UIKit框架(四十五) —— 支持DarkMode的简单示例(一)
    46. UIKit框架(四十六) —— 支持DarkMode的简单示例(二)
    47. UIKit框架(四十七) —— 自定义Calendar Control的简单示例(一)

    源码

    1. Swift

    首先看下工程组织结构

    下面就是源码了

    1. SceneDelegate.swift
    
    import UIKit
    
    class SceneDelegate: UIResponder, UIWindowSceneDelegate {
      var window: UIWindow?
    
      func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
    
        #if targetEnvironment(macCatalyst)
        windowScene.titlebar?.titleVisibility = .hidden
        #endif
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = UINavigationController(rootViewController: ItemListViewController())
        window?.makeKeyAndVisible()
      }
    }
    
    2. ChecklistItemTableViewCell.swift
    
    import UIKit
    
    class ChecklistItemTableViewCell: UITableViewCell {
      lazy var titleLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = UIFont.preferredFont(forTextStyle: .headline)
        label.textColor = .label
        label.numberOfLines = 0
        label.adjustsFontForContentSizeCategory = true
        label.accessibilityTraits = .button
        label.accessibilityHint = "Double tap to open"
        label.isAccessibilityElement = true
        return label
      }()
    
      lazy var subtitleLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = UIFont.preferredFont(forTextStyle: .subheadline)
        label.textColor = .secondaryLabel
        label.numberOfLines = 1
        label.adjustsFontForContentSizeCategory = true
        label.isAccessibilityElement = false
        return label
      }()
    
      lazy var completionButton: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.contentMode = .scaleAspectFit
    
        let configuration = UIImage.SymbolConfiguration(scale: .large)
        let image = UIImage(systemName: "square", withConfiguration: configuration)
        button.setImage(image, for: .normal)
        button.tintColor = UIColor.systemBlue
        button.isUserInteractionEnabled = true
        button.isAccessibilityElement = true
        button.accessibilityTraits = .button
        button.accessibilityLabel = "Mark as Complete"
        return button
      }()
    
      private lazy var dateFormatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .long
        return dateFormatter
      }()
    
      var item: ChecklistItem? {
        didSet {
          guard let item = item else {
            return
          }
    
          let subtitleText = dateFormatter.string(from: item.date)
    
          titleLabel.text = item.title
          subtitleLabel.text = subtitleText
    
          // The title acts as the cell to voice over because marking the cell as the accessibility element would prevent the checkmark box from being discovered by VoiceOver and other accessibility technologies
          titleLabel.accessibilityLabel = "\(item.title)\n\(subtitleText)"
    
          updateCompletionStatusAccessibilityInformation()
        }
      }
    
      static let reuseIdentifier = String(describing: ChecklistItemTableViewCell.self)
    
      override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    
        selectionStyle = .none
    
        addSubview(titleLabel)
        addSubview(subtitleLabel)
        addSubview(completionButton)
    
        completionButton.addTarget(self, action: #selector(userDidTapOnCheckmarkBox), for: .touchUpInside)
    
        layoutSubviews()
      }
    
      required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    
      override func layoutSubviews() {
        super.layoutSubviews()
    
        if traitCollection.horizontalSizeClass == .compact {
          NSLayoutConstraint.activate([
            titleLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor, constant: 4),
            completionButton.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor, constant: -4)
          ])
        } else {
          NSLayoutConstraint.activate([
            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
            completionButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16)
          ])
        }
    
        NSLayoutConstraint.activate([
          titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10),
          titleLabel.trailingAnchor.constraint(equalTo: completionButton.leadingAnchor, constant: -5),
    
          subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor),
          subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
          subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
          subtitleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10),
    
          completionButton.centerYAnchor.constraint(equalTo: centerYAnchor),
          completionButton.widthAnchor.constraint(equalToConstant: 30),
          completionButton.heightAnchor.constraint(equalToConstant: 30)
        ])
      }
    
      @objc func userDidTapOnCheckmarkBox() {
        guard let item = item else {
          return
        }
    
        item.completed.toggle()
    
        updateCompletionStatusAccessibilityInformation()
    
        UIView.transition(
          with: completionButton,
          duration: 0.2,
          options: .transitionCrossDissolve,
          animations: {
          let symbolName: String
    
          if item.completed {
            symbolName = "checkmark.square"
          } else {
            symbolName = "square"
          }
    
          let configuration = UIImage.SymbolConfiguration(scale: .large)
          let image = UIImage(systemName: symbolName, withConfiguration: configuration)
          self.completionButton.setImage(image, for: .normal)
          },
          completion: nil)
      }
    
      private func updateCompletionStatusAccessibilityInformation() {
        if item?.completed == true {
          completionButton.accessibilityLabel = "Mark as incomplete"
        } else {
          completionButton.accessibilityLabel = "Mark as complete"
        }
      }
    
      override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
    
        layoutSubviews()
      }
    }
    
    3. ItemListViewController.swift
    
    import UIKit
    
    class ItemListViewController: UITableViewController {
      // MARK: Diffable Data Source Setup
    
      enum Section {
        case main
      }
    
      typealias DataSource = UITableViewDiffableDataSource<Section, ChecklistItem>
      typealias Snapshot = NSDiffableDataSourceSnapshot<Section, ChecklistItem>
    
      // MARK: Properties
    
      private lazy var dataSource = makeDataSource()
      private lazy var items = ChecklistItem.exampleItems
    
      private var searchQuery: String? = nil {
        didSet {
          applySnapshot()
        }
      }
    
      // MARK: Controller Setup
    
      private lazy var searchController = makeSearchController()
    
      override func viewDidLoad() {
        super.viewDidLoad()
    
        view.backgroundColor = .systemBackground
    
        navigationItem.largeTitleDisplayMode = .automatic
        navigationController?.navigationBar.prefersLargeTitles = true
        title = "Checkmate"
    
        navigationItem.searchController = searchController
        definesPresentationContext = true
    
        navigationItem.rightBarButtonItem = UIBarButtonItem(
          image: UIImage(systemName: "plus.circle"),
          style: .done,
          target: self,
          action: #selector(didTapNewItemButton)
        )
        navigationItem.rightBarButtonItem?.accessibilityLabel = "New Item"
    
        tableView.register(
          ChecklistItemTableViewCell.self,
          forCellReuseIdentifier: ChecklistItemTableViewCell.reuseIdentifier)
    
        tableView.tableFooterView = UIView()
      }
    
      override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    
        applySnapshot(animatingDifferences: false)
      }
    
      @objc func didTapNewItemButton() {
        let newItemAlert = UIAlertController(
          title: "New Item",
          message: "What would you like to do today?",
          preferredStyle: .alert)
        newItemAlert.addTextField { textField in
          textField.placeholder = "Item Text"
        }
        newItemAlert.addAction(UIAlertAction(title: "Create Item", style: .default) { [weak self] _ in
          guard let self = self else { return }
    
          guard
            let title = newItemAlert.textFields?[0].text,
            !title.isEmpty
            else {
              let errorAlert = UIAlertController(
                title: "Error",
                message: "You can't leave the title empty.",
                preferredStyle: .alert)
              errorAlert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
              self.present(errorAlert, animated: true, completion: nil)
              return
          }
    
          self.items.append(
            ChecklistItem(title: title, date: Date())
          )
    
          self.applySnapshot()
        })
        newItemAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        present(newItemAlert, animated: true, completion: nil)
      }
    }
    
    // MARK: Table View Methods
    
    extension ItemListViewController {
      func applySnapshot(animatingDifferences: Bool = true) {
        var items: [ChecklistItem] = self.items
    
        if let searchQuery = searchQuery, !searchQuery.isEmpty {
          items = items.filter { item in
            return item.title.lowercased().contains(searchQuery.lowercased())
          }
        }
    
        items = items.sorted { one, two in
          return one.date < two.date
        }
        var snapshot = Snapshot()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
      }
    
      func makeDataSource() -> DataSource {
        DataSource(tableView: tableView) { tableView, indexPath, item in
          let cell = tableView.dequeueReusableCell(
            withIdentifier: ChecklistItemTableViewCell.reuseIdentifier,
            for: indexPath) as? ChecklistItemTableViewCell
          cell?.item = item
          return cell
        }
      }
    
      override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        navigationController?.pushViewController(ItemDetailViewController(item: items[indexPath.row]), animated: true)
      }
    
      // MARK: Contexual Menus
    
      override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
        let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
          guard
            let self = self,
            let item = self.dataSource.itemIdentifier(for: indexPath)
            else {
              return nil
          }
    
          let deleteAction = UIAction(
            title: "Delete Item",
            image: UIImage(systemName: "trash"),
            attributes: .destructive) { _ in
              self.items.removeAll { existingItem in
                return existingItem == item
              }
    
              self.applySnapshot()
          }
    
          return UIMenu(title: item.title.truncatedPrefix(12), image: nil, children: [deleteAction])
        }
    
        return configuration
      }
    }
    
    // MARK: Search Controller Setup
    
    extension ItemListViewController: UISearchResultsUpdating {
      func makeSearchController() -> UISearchController {
        let controller = UISearchController(searchResultsController: nil)
        controller.searchResultsUpdater = self
        controller.obscuresBackgroundDuringPresentation = false
        controller.searchBar.placeholder = "Search Items"
        return controller
      }
    
      func updateSearchResults(for searchController: UISearchController) {
        searchQuery = searchController.searchBar.text
      }
    }
    
    4. ItemDetailViewController.swift
    
    import UIKit
    
    class ItemDetailViewController: UITableViewController {
      lazy var dateFormatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .long
        return dateFormatter
      }()
    
      let item: ChecklistItem
    
      init(item: ChecklistItem) {
        self.item = item
    
        super.init(style: .insetGrouped)
      }
    
      required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    
      override func viewDidLoad() {
        super.viewDidLoad()
    
        view.backgroundColor = .systemGroupedBackground
    
        title = String(item.title.truncatedPrefix(16))
    
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
      }
    
      override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        2
      }
    
      override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .value1, reuseIdentifier: "cell")
    
        if indexPath.row == 0 {
          cell.textLabel?.text = "Task Name"
          cell.detailTextLabel?.text = item.title
        } else {
          cell.textLabel?.text = "Due Date"
          cell.detailTextLabel?.text = dateFormatter.string(from: item.date)
        }
    
        cell.accessoryType = .disclosureIndicator
    
        return cell
      }
    
      override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    
        if indexPath.row == 0 {
          let changeTaskNameAlert = UIAlertController(
            title: "Edit Name",
            message: "What should this task be called?",
            preferredStyle: .alert)
          changeTaskNameAlert.addTextField { [weak self] textField in
            guard let self = self else { return }
    
            textField.text = self.item.title
            textField.placeholder = "Task Name"
          }
          changeTaskNameAlert.addAction(UIAlertAction(title: "Save", style: .default) { [weak self] _ in
            guard let self = self else { return }
    
            guard
              let newTitle = changeTaskNameAlert.textFields?[0].text,
              !newTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
              else {
                return
            }
    
            self.item.title = newTitle
            self.tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .fade)
          })
    
          changeTaskNameAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
          present(changeTaskNameAlert, animated: true, completion: nil)
        } else if indexPath.row == 1 {
          let pickerController = CalendarPickerViewController(
            baseDate: item.date,
            selectedDateChanged: { [weak self] date in
            guard let self = self else { return }
    
            self.item.date = date
            self.tableView.reloadRows(at: [IndexPath(row: 1, section: 0)], with: .fade)
            })
    
          present(pickerController, animated: true, completion: nil)
        }
      }
    }
    
    5. MonthMetadata.swift
    
    import Foundation
    
    struct MonthMetadata {
      let numberOfDays: Int
      let firstDay: Date
      let firstDayWeekday: Int
    }
    
    6. ChecklistItem.swift
    
    import Foundation
    
    class ChecklistItem: Hashable {
      var id: UUID
      var title: String
      var date: Date
      var completed: Bool
    
      init(title: String, date: Date, completed: Bool = false) {
        self.id = UUID()
        self.title = title
        self.date = date
        self.completed = completed
      }
    
      func hash(into hasher: inout Hasher) {
        hasher.combine(id)
      }
    
      static func == (lhs: ChecklistItem, rhs: ChecklistItem) -> Bool {
        return lhs.id == rhs.id
      }
    }
    
    extension ChecklistItem {
      static var exampleItems: [ChecklistItem] = [
        ChecklistItem(title: "Complete the Diffable Data Sources tutorial on raywenderlich.com", date: Date())
      ]
    }
    
    7. Day.swift
    
    import Foundation
    
    struct Day {
      // 1
      let date: Date
      // 2
      let number: String
      // 3
      let isSelected: Bool
      // 4
      let isWithinDisplayedMonth: Bool
    }
    
    8. CalendarPickerViewController.swift
    
    import UIKit
    
    class CalendarPickerViewController: UIViewController {
      // MARK: Views
      private lazy var dimmedBackgroundView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = UIColor.black.withAlphaComponent(0.3)
        return view
      }()
    
      private lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.minimumLineSpacing = 0
        layout.minimumInteritemSpacing = 0
    
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.isScrollEnabled = false
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
      }()
    
      private lazy var headerView = CalendarPickerHeaderView { [weak self] in
        guard let self = self else { return }
    
        self.dismiss(animated: true)
      }
    
      private lazy var footerView = CalendarPickerFooterView(
        didTapLastMonthCompletionHandler: { [weak self] in
        guard let self = self else { return }
    
        self.baseDate = self.calendar.date(
          byAdding: .month,
          value: -1,
          to: self.baseDate
          ) ?? self.baseDate
        },
        didTapNextMonthCompletionHandler: { [weak self] in
          guard let self = self else { return }
    
          self.baseDate = self.calendar.date(
            byAdding: .month,
            value: 1,
            to: self.baseDate
            ) ?? self.baseDate
        })
    
      // MARK: Calendar Data Values
    
      private let selectedDate: Date
      private var baseDate: Date {
        didSet {
          days = generateDaysInMonth(for: baseDate)
          collectionView.reloadData()
          headerView.baseDate = baseDate
        }
      }
    
      private lazy var days = generateDaysInMonth(for: baseDate)
    
      private var numberOfWeeksInBaseDate: Int {
        calendar.range(of: .weekOfMonth, in: .month, for: baseDate)?.count ?? 0
      }
    
      private let selectedDateChanged: ((Date) -> Void)
      private let calendar = Calendar(identifier: .gregorian)
    
      private lazy var dateFormatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "d"
        return dateFormatter
      }()
    
      // MARK: Initializers
    
      init(baseDate: Date, selectedDateChanged: @escaping ((Date) -> Void)) {
        self.selectedDate = baseDate
        self.baseDate = baseDate
        self.selectedDateChanged = selectedDateChanged
    
        super.init(nibName: nil, bundle: nil)
    
        modalPresentationStyle = .overCurrentContext
        modalTransitionStyle = .crossDissolve
        definesPresentationContext = true
      }
    
      required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    
      // MARK: View Lifecycle
    
      override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.backgroundColor = .systemGroupedBackground
    
        view.addSubview(dimmedBackgroundView)
        view.addSubview(collectionView)
        view.addSubview(headerView)
        view.addSubview(footerView)
    
        var constraints = [
          dimmedBackgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
          dimmedBackgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
          dimmedBackgroundView.topAnchor.constraint(equalTo: view.topAnchor),
          dimmedBackgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ]
    
        constraints.append(contentsOf: [
          //1
          collectionView.leadingAnchor.constraint(
            equalTo: view.readableContentGuide.leadingAnchor),
          collectionView.trailingAnchor.constraint(
            equalTo: view.readableContentGuide.trailingAnchor),
          //2
          collectionView.centerYAnchor.constraint(
            equalTo: view.centerYAnchor,
            constant: 10),
          //3
          collectionView.heightAnchor.constraint(
            equalTo: view.heightAnchor,
            multiplier: 0.5)
        ])
    
        constraints.append(contentsOf: [
          headerView.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor),
          headerView.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor),
          headerView.bottomAnchor.constraint(equalTo: collectionView.topAnchor),
          headerView.heightAnchor.constraint(equalToConstant: 85),
          
          footerView.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor),
          footerView.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor),
          footerView.topAnchor.constraint(equalTo: collectionView.bottomAnchor),
          footerView.heightAnchor.constraint(equalToConstant: 60)
        ])
    
        NSLayoutConstraint.activate(constraints)
    
        collectionView.register(
          CalendarDateCollectionViewCell.self,
          forCellWithReuseIdentifier: CalendarDateCollectionViewCell.reuseIdentifier
        )
    
        collectionView.dataSource = self
        collectionView.delegate = self
        headerView.baseDate = baseDate
      }
    
      override func viewWillTransition(
        to size: CGSize,
        with coordinator: UIViewControllerTransitionCoordinator
      ) {
        super.viewWillTransition(to: size, with: coordinator)
        collectionView.reloadData()
      }
    }
    
    // MARK: - Day Generation
    private extension CalendarPickerViewController {
      // 1
      func monthMetadata(for baseDate: Date) throws -> MonthMetadata {
        // 2
        guard
          let numberOfDaysInMonth = calendar.range(
            of: .day,
            in: .month,
            for: baseDate)?.count,
          let firstDayOfMonth = calendar.date(
            from: calendar.dateComponents([.year, .month], from: baseDate))
          else {
            // 3
            throw CalendarDataError.metadataGeneration
        }
    
        // 4
        let firstDayWeekday = calendar.component(.weekday, from: firstDayOfMonth)
    
        // 5
        return MonthMetadata(
          numberOfDays: numberOfDaysInMonth,
          firstDay: firstDayOfMonth,
          firstDayWeekday: firstDayWeekday)
      }
    
      // 1
      func generateDaysInMonth(for baseDate: Date) -> [Day] {
        // 2
        guard let metadata = try? monthMetadata(for: baseDate) else {
          preconditionFailure("An error occurred when generating the metadata for \(baseDate)")
        }
    
        let numberOfDaysInMonth = metadata.numberOfDays
        let offsetInInitialRow = metadata.firstDayWeekday
        let firstDayOfMonth = metadata.firstDay
    
        // 3
        var days: [Day] = (1..<(numberOfDaysInMonth + offsetInInitialRow))
          .map { day in
            // 4
            let isWithinDisplayedMonth = day >= offsetInInitialRow
            // 5
            let dayOffset =
              isWithinDisplayedMonth ?
              day - offsetInInitialRow :
              -(offsetInInitialRow - day)
    
            // 6
            return generateDay(
              offsetBy: dayOffset,
              for: firstDayOfMonth,
              isWithinDisplayedMonth: isWithinDisplayedMonth)
          }
    
        days += generateStartOfNextMonth(using: firstDayOfMonth)
    
        return days
      }
    
      // 7
      func generateDay(
        offsetBy dayOffset: Int,
        for baseDate: Date,
        isWithinDisplayedMonth: Bool
      ) -> Day {
        let date = calendar.date(
          byAdding: .day,
          value: dayOffset,
          to: baseDate)
          ?? baseDate
    
        return Day(
          date: date,
          number: dateFormatter.string(from: date),
          isSelected: calendar.isDate(date, inSameDayAs: selectedDate),
          isWithinDisplayedMonth: isWithinDisplayedMonth
        )
      }
    
      // 1
      func generateStartOfNextMonth(
        using firstDayOfDisplayedMonth: Date
        ) -> [Day] {
        // 2
        guard
          let lastDayInMonth = calendar.date(
            byAdding: DateComponents(month: 1, day: -1),
            to: firstDayOfDisplayedMonth)
          else {
            return []
        }
    
        // 3
        let additionalDays = 7 - calendar.component(.weekday, from: lastDayInMonth)
        guard additionalDays > 0 else {
          return []
        }
    
        // 4
        let days: [Day] = (1...additionalDays)
          .map {
            generateDay(
            offsetBy: $0,
            for: lastDayInMonth,
            isWithinDisplayedMonth: false)
          }
    
        return days
      }
    
      enum CalendarDataError: Error {
        case metadataGeneration
      }
    }
    
    // MARK: - UICollectionViewDataSource
    extension CalendarPickerViewController: UICollectionViewDataSource {
      func collectionView(
        _ collectionView: UICollectionView,
        numberOfItemsInSection section: Int
      ) -> Int {
        days.count
      }
    
      func collectionView(
        _ collectionView: UICollectionView,
        cellForItemAt indexPath: IndexPath
      ) -> UICollectionViewCell {
        let day = days[indexPath.row]
    
        let cell = collectionView.dequeueReusableCell(
          withReuseIdentifier: CalendarDateCollectionViewCell.reuseIdentifier,
          for: indexPath) as! CalendarDateCollectionViewCell
        // swiftlint:disable:previous force_cast
    
        cell.day = day
        return cell
      }
    }
    
    // MARK: - UICollectionViewDelegateFlowLayout
    extension CalendarPickerViewController: UICollectionViewDelegateFlowLayout {
      func collectionView(
        _ collectionView: UICollectionView,
        didSelectItemAt indexPath: IndexPath
      ) {
        let day = days[indexPath.row]
        selectedDateChanged(day.date)
        dismiss(animated: true, completion: nil)
      }
    
      func collectionView(
        _ collectionView: UICollectionView,
        layout collectionViewLayout: UICollectionViewLayout,
        sizeForItemAt indexPath: IndexPath
      ) -> CGSize {
        let width = Int(collectionView.frame.width / 7)
        let height = Int(collectionView.frame.height) / numberOfWeeksInBaseDate
        return CGSize(width: width, height: height)
      }
    }
    
    9. CalendarPickerFooterView.swift
    
    import UIKit
    
    class CalendarPickerFooterView: UIView {
      lazy var separatorView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = UIColor.label.withAlphaComponent(0.2)
        return view
      }()
    
      lazy var previousMonthButton: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.titleLabel?.font = .systemFont(ofSize: 17, weight: .medium)
        button.titleLabel?.textAlignment = .left
    
        if let chevronImage = UIImage(systemName: "chevron.left.circle.fill") {
          let imageAttachment = NSTextAttachment(image: chevronImage)
          let attributedString = NSMutableAttributedString()
    
          attributedString.append(
            NSAttributedString(attachment: imageAttachment)
          )
    
          attributedString.append(
            NSAttributedString(string: " Previous")
          )
    
          button.setAttributedTitle(attributedString, for: .normal)
        } else {
          button.setTitle("Previous", for: .normal)
        }
    
        button.titleLabel?.textColor = .label
    
        button.addTarget(self, action: #selector(didTapPreviousMonthButton), for: .touchUpInside)
        return button
      }()
    
      lazy var nextMonthButton: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.titleLabel?.font = .systemFont(ofSize: 17, weight: .medium)
        button.titleLabel?.textAlignment = .right
    
        if let chevronImage = UIImage(systemName: "chevron.right.circle.fill") {
          let imageAttachment = NSTextAttachment(image: chevronImage)
          let attributedString = NSMutableAttributedString(string: "Next ")
    
          attributedString.append(
            NSAttributedString(attachment: imageAttachment)
          )
    
          button.setAttributedTitle(attributedString, for: .normal)
        } else {
          button.setTitle("Next", for: .normal)
        }
    
        button.titleLabel?.textColor = .label
    
        button.addTarget(self, action: #selector(didTapNextMonthButton), for: .touchUpInside)
        return button
      }()
    
      let didTapLastMonthCompletionHandler: (() -> Void)
      let didTapNextMonthCompletionHandler: (() -> Void)
    
      init(
        didTapLastMonthCompletionHandler: @escaping (() -> Void),
        didTapNextMonthCompletionHandler: @escaping (() -> Void)
      ) {
        self.didTapLastMonthCompletionHandler = didTapLastMonthCompletionHandler
        self.didTapNextMonthCompletionHandler = didTapNextMonthCompletionHandler
    
        super.init(frame: CGRect.zero)
    
        translatesAutoresizingMaskIntoConstraints = false
        backgroundColor = .systemGroupedBackground
    
        layer.maskedCorners = [
          .layerMinXMaxYCorner,
          .layerMaxXMaxYCorner
        ]
        layer.cornerCurve = .continuous
        layer.cornerRadius = 15
    
        addSubview(separatorView)
        addSubview(previousMonthButton)
        addSubview(nextMonthButton)
      }
    
      required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    
      private var previousOrientation: UIDeviceOrientation = UIDevice.current.orientation
    
      override func layoutSubviews() {
        super.layoutSubviews()
    
        let smallDevice = UIScreen.main.bounds.width <= 350
    
        let fontPointSize: CGFloat = smallDevice ? 14 : 17
    
        previousMonthButton.titleLabel?.font = .systemFont(ofSize: fontPointSize, weight: .medium)
        nextMonthButton.titleLabel?.font = .systemFont(ofSize: fontPointSize, weight: .medium)
    
        NSLayoutConstraint.activate([
          separatorView.leadingAnchor.constraint(equalTo: leadingAnchor),
          separatorView.trailingAnchor.constraint(equalTo: trailingAnchor),
          separatorView.topAnchor.constraint(equalTo: topAnchor),
          separatorView.heightAnchor.constraint(equalToConstant: 1),
    
          previousMonthButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
          previousMonthButton.centerYAnchor.constraint(equalTo: centerYAnchor),
    
          nextMonthButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
          nextMonthButton.centerYAnchor.constraint(equalTo: centerYAnchor)
        ])
      }
    
      @objc func didTapPreviousMonthButton() {
        didTapLastMonthCompletionHandler()
      }
    
      @objc func didTapNextMonthButton() {
        didTapNextMonthCompletionHandler()
      }
    }
    
    10. CalendarPickerHeaderView.swift
    
    import UIKit
    
    class CalendarPickerHeaderView: UIView {
      lazy var monthLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .systemFont(ofSize: 26, weight: .bold)
        label.text = "Month"
        label.accessibilityTraits = .header
        label.isAccessibilityElement = true
        return label
      }()
    
      lazy var closeButton: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
    
        let configuration = UIImage.SymbolConfiguration(scale: .large)
        let image = UIImage(systemName: "xmark.circle.fill", withConfiguration: configuration)
        button.setImage(image, for: .normal)
    
        button.tintColor = .secondaryLabel
        button.contentMode = .scaleAspectFill
        button.isUserInteractionEnabled = true
        button.isAccessibilityElement = true
        button.accessibilityLabel = "Close Picker"
        return button
      }()
    
      lazy var dayOfWeekStackView: UIStackView = {
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.distribution = .fillEqually
        return stackView
      }()
    
      lazy var separatorView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = UIColor.label.withAlphaComponent(0.2)
        return view
      }()
    
      private lazy var dateFormatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.calendar = Calendar(identifier: .gregorian)
        dateFormatter.locale = Locale.autoupdatingCurrent
        dateFormatter.setLocalizedDateFormatFromTemplate("MMMM y")
        return dateFormatter
      }()
    
      var baseDate = Date() {
        didSet {
          monthLabel.text = dateFormatter.string(from: baseDate)
        }
      }
    
      var exitButtonTappedCompletionHandler: (() -> Void)
    
      init(exitButtonTappedCompletionHandler: @escaping (() -> Void)) {
        self.exitButtonTappedCompletionHandler = exitButtonTappedCompletionHandler
    
        super.init(frame: CGRect.zero)
    
        translatesAutoresizingMaskIntoConstraints = false
    
        backgroundColor = .systemGroupedBackground
    
        layer.maskedCorners = [
          .layerMinXMinYCorner,
          .layerMaxXMinYCorner
        ]
        layer.cornerCurve = .continuous
        layer.cornerRadius = 15
    
        addSubview(monthLabel)
        addSubview(closeButton)
        addSubview(dayOfWeekStackView)
        addSubview(separatorView)
    
        for dayNumber in 1...7 {
          let dayLabel = UILabel()
          dayLabel.font = .systemFont(ofSize: 12, weight: .bold)
          dayLabel.textColor = .secondaryLabel
          dayLabel.textAlignment = .center
          dayLabel.text = dayOfWeekLetter(for: dayNumber)
    
          // VoiceOver users don't need to hear these days of the week read to them, nor do SwitchControl or Voice Control users need to select them
          // If fact, they get in the way!
          // When a VoiceOver user highlights a day of the month, the day of the week is read to them.
          // That method provides the same amount of context as this stack view does to visual users
          dayLabel.isAccessibilityElement = false
          dayOfWeekStackView.addArrangedSubview(dayLabel)
        }
    
        closeButton.addTarget(self, action: #selector(didTapExitButton), for: .touchUpInside)
      }
    
      @objc func didTapExitButton() {
        exitButtonTappedCompletionHandler()
      }
    
      required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    
      private func dayOfWeekLetter(for dayNumber: Int) -> String {
        switch dayNumber {
        case 1:
          return "S"
        case 2:
          return "M"
        case 3:
          return "T"
        case 4:
          return "W"
        case 5:
          return "T"
        case 6:
          return "F"
        case 7:
          return "S"
        default:
          return ""
        }
      }
    
      override func layoutSubviews() {
        super.layoutSubviews()
    
        NSLayoutConstraint.activate([
          monthLabel.topAnchor.constraint(equalTo: topAnchor, constant: 15),
          monthLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
          monthLabel.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: 5),
    
          closeButton.centerYAnchor.constraint(equalTo: monthLabel.centerYAnchor),
          closeButton.heightAnchor.constraint(equalToConstant: 28),
          closeButton.widthAnchor.constraint(equalToConstant: 28),
          closeButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -15),
    
          dayOfWeekStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
          dayOfWeekStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
          dayOfWeekStackView.bottomAnchor.constraint(equalTo: separatorView.bottomAnchor, constant: -5),
    
          separatorView.leadingAnchor.constraint(equalTo: leadingAnchor),
          separatorView.trailingAnchor.constraint(equalTo: trailingAnchor),
          separatorView.bottomAnchor.constraint(equalTo: bottomAnchor),
          separatorView.heightAnchor.constraint(equalToConstant: 1)
        ])
      }
    }
    
    11. CalendarDateCollectionViewCell.swift
    
    import UIKit
    
    class CalendarDateCollectionViewCell: UICollectionViewCell {
      private lazy var selectionBackgroundView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.clipsToBounds = true
        view.backgroundColor = .systemRed
        return view
      }()
    
      private lazy var numberLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.textAlignment = .center
        label.font = UIFont.systemFont(ofSize: 18, weight: .medium)
        label.textColor = .label
        return label
      }()
    
      private lazy var accessibilityDateFormatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.calendar = Calendar(identifier: .gregorian)
        dateFormatter.setLocalizedDateFormatFromTemplate("EEEE, MMMM d")
        return dateFormatter
      }()
    
      static let reuseIdentifier = String(describing: CalendarDateCollectionViewCell.self)
    
      var day: Day? {
        didSet {
          guard let day = day else { return }
    
          numberLabel.text = day.number
          accessibilityLabel = accessibilityDateFormatter.string(from: day.date)
          updateSelectionStatus()
        }
      }
    
      override init(frame: CGRect) {
        super.init(frame: frame)
    
        isAccessibilityElement = true
        accessibilityTraits = .button
    
        contentView.addSubview(selectionBackgroundView)
        contentView.addSubview(numberLabel)
      }
    
      required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    
      override func layoutSubviews() {
        super.layoutSubviews()
    
        // This allows for rotations and trait collection
        // changes (e.g. entering split view on iPad) to update constraints correctly.
        // Removing old constraints allows for new ones to be created
        // regardless of the values of the old ones
        NSLayoutConstraint.deactivate(selectionBackgroundView.constraints)
    
        // 1
        let size = traitCollection.horizontalSizeClass == .compact ?
          min(min(frame.width, frame.height) - 10, 60) : 45
    
        // 2
        NSLayoutConstraint.activate([
          numberLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
          numberLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
    
          selectionBackgroundView.centerYAnchor.constraint(equalTo: numberLabel.centerYAnchor),
          selectionBackgroundView.centerXAnchor.constraint(equalTo: numberLabel.centerXAnchor),
          selectionBackgroundView.widthAnchor.constraint(equalToConstant: size),
          selectionBackgroundView.heightAnchor.constraint(equalTo: selectionBackgroundView.widthAnchor)
        ])
    
        selectionBackgroundView.layer.cornerRadius = size / 2
      }
    
      override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
    
        layoutSubviews()
      }
    }
    
    // MARK: - Appearance
    private extension CalendarDateCollectionViewCell {
      // 1
      func updateSelectionStatus() {
        guard let day = day else { return }
    
        if day.isSelected {
          applySelectedStyle()
        } else {
          applyDefaultStyle(isWithinDisplayedMonth: day.isWithinDisplayedMonth)
        }
      }
    
      // 2
      var isSmallScreenSize: Bool {
        let isCompact = traitCollection.horizontalSizeClass == .compact
        let smallWidth = UIScreen.main.bounds.width <= 350
        let widthGreaterThanHeight = UIScreen.main.bounds.width > UIScreen.main.bounds.height
    
        return isCompact && (smallWidth || widthGreaterThanHeight)
      }
    
      // 3
      func applySelectedStyle() {
        accessibilityTraits.insert(.selected)
        accessibilityHint = nil
    
        numberLabel.textColor = isSmallScreenSize ? .systemRed : .white
        selectionBackgroundView.isHidden = isSmallScreenSize
      }
    
      // 4
      func applyDefaultStyle(isWithinDisplayedMonth: Bool) {
        accessibilityTraits.remove(.selected)
        accessibilityHint = "Tap to select"
    
        numberLabel.textColor = isWithinDisplayedMonth ? .label : .secondaryLabel
        selectionBackgroundView.isHidden = true
      }
    }
    
    12. String+Truncate.swift
    
    import Foundation
    
    extension String {
      func truncatedPrefix(_ maxLength: Int, using truncator: String = "...") -> String {
        "\(prefix(maxLength))\(truncator)"
      }
    }
    

    后记

    本篇主要讲述了自定义Calendar Control的简单示例的简单示例,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

          本文标题:UIKit框架(四十八) —— 自定义Calendar Cont

          本文链接:https://www.haomeiwen.com/subject/vwotsktx.html