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

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

作者: 刀客传奇 | 来源:发表于2020-08-26 18:20 被阅读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的简单示例(二)

    开始

    首先看下主要内容:

    在本calendar UI control教程中,您将构建一个iOS控件,该控件可在与日期进行交互时为用户提供至关重要的清晰度和上下文。内容来自翻译

    接着看下写作环境:

    Swift 5, iOS 13, Xcode 11

    为用户提供选择日期的方式是移动应用程序的常用功能。有时,您只需要使用内置的UIDatePicker,但是如果您想要更多自定义的东西怎么办?

    尽管UIDatePicker可以很好地完成基本任务,但它缺乏指导用户选择日期的重要环境。在日常生活中,您可以使用日历来跟踪日期。日历比UIDatePicker提供更多的上下文,因为它可以告诉您日期是星期几。

    在本教程中,您将构建一个自定义的日历UI控件,该控件在选择日期时增加了至关重要的清晰度和上下文。在此过程中,您将学习如何:

    • 使用Foundation框架提供的Calendar API生成和处理日期。
    • UICollectionView中显示数据。
    • 使控件可以访问辅助技术,例如VoiceOver

    你准备好了吗?下面我们继续!

    注意:本教程假定您了解UICollectionView的基础知识。如果您不熟悉iOS开发,请查看我们的UICollectionView Tutorial: Getting Started

    示例项目Checkmate概述了类似提醒的清单应用程序,该应用程序允许用户创建任务并设置其截止日期。

    打开启动项目。 然后,构建并运行。

    该应用程序显示任务列表。 点击Complete the Diffable Data Sources tutorial on raywenderlich.com

    将打开一个详细信息屏幕,其中显示任务的名称和截止日期。

    点击Due Date。 目前没有任何反应,但是很快,点击此处将显示您的日历控件。


    Breaking Down the UI

    以下是完整控件的外观的屏幕截图:

    日历控件由三部分组成:

    • Green, Header view:显示当前月份和年份,允许用户关闭选择器并显示工作日标签。
    • Blue, Month view:显示每月的日期以及当前选择的日期。
    • Pink, Footer view:允许用户选择不同的月份。

    您将首先处理month view


    Creating the Month View

    打开CalendarPicker文件夹内的CalendarPickerViewController.swift

    该文件当前包含dimmedBackgroundView(一个用于将日历选择器从背景中升高)的透明黑色视图,以及其他样板代码。

    首先,创建一个UICollectionView。 在dimmedBackgroundView的定义下面,输入以下代码:

    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
    }()
    

    在上面的代码中,您将创建一个在其单元格之间没有间距的collection view,并禁用其滚动。 您还将禁用将其自动调整大小的蒙版自动转换为约束的功能,因为您将为此创建自己的约束。

    很棒! 接下来,您将集合视图添加到视图层次结构,并设置其Auto Layout constraints


    Setting up the Collection View

    viewDidLoad()super.viewDidLoad()下面,设置collection view的背景色:

    collectionView.backgroundColor = .systemGroupedBackground
    

    接下来,在view.addSubview(dimmedBackgroundView)下面,将集合视图添加到视图层次结构中:

    view.addSubview(collectionView)
    

    最后,您将给它四个约束。 在对NSLayoutConstraint.activate(_ :)的现有调用之前,添加以下约束:

    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)
    ])
    

    这看起来很复杂,但不要惊慌! 使用此代码,您:

    • 1) 将collection view的前缘(左)和后缘(右)约束到该视图的可读内容指南的前缘和后缘。
    • 2) 将collection view在视图控制器内垂直居中,向下移动10个点。
    • 3) 将collection view的高度设置为视图控制器高度的一半。

    构建并运行。 打开日历选择器,然后…

    成功! 好吧... 现在没有什么可看的,但是当您为日历生成数据时,稍后再解决。


    Breaking Down the Data

    要显示一个月,您需要一个日期列表。 在的Models文件夹中创建一个新文件名为Day.swift

    在此文件中,输入以下代码:

    struct Day {
      // 1 
      let date: Date 
      // 2
      let number: String 
      // 3
      let isSelected: Bool
      // 4
      let isWithinDisplayedMonth: Bool
    }
    

    那么这些属性是做什么用的呢? 这是每一行的工作:

    • 1) Date表示一个月中的给定日期。
    • 2) 在collection view cell上显示的数字。
    • 3) 跟踪是否选择了该日期。
    • 4) 跟踪此日期是否在当前查看的月份内。

    太好了,您现在可以使用数据模型了!


    Using the Calendar API

    下一步是获取对Calendar的引用。 Calendar FoundationFoundation的一部分,它使您可以更改和访问有关Date的信息。

    打开CalendarPickerViewController.swift。 在selectedDateChanged下,创建一个新的Calendar

    private let calendar = Calendar(identifier: .gregorian)
    

    calendar identifier设置为.gregorian意味着Calendar API应该使用公历。 公历是世界上使用最广泛的日历,包括Apple的Calendar应用程序。

    注意:唯一不使用公历的国家是埃塞俄比亚,尼泊尔,伊朗和阿富汗。 因此,如果您的应用针对这些国家之一的用户,则可能需要针对其日历系统调整本教程。

    现在您有了日历对象,是时候使用它来生成一些数据了。


    Generating a Month’s Metadata

    CalendarPickerViewController.swift的底部,添加以下私有扩展:

    // 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)
      }
    
      enum CalendarDataError: Error {
        case metadataGeneration
      }
    }
    

    现在,要分解此代码:

    • 1) 首先,定义一个名为monthMetadata(for :)的方法,该方法接受一个Date并返回MonthMetadata。 项目中已经存在MonthMetadata,因此无需创建它。
    • 2) 您向日历询问在baseDate月份中的天数,然后得到该月的第一天。
    • 3) 先前的两个调用均返回可选值。 如果任何一个返回nil,则代码将引发错误并返回。
    • 4) 您将获得工作日值,该数字介于1到7之间,代表一个月的第一天位于星期几。
    • 5) 最后,使用这些值创建MonthMetadata的实例并返回它。

    很好! 该元数据为您提供了生成月份中的每一天所需的所有信息,以及一些额外的信息。


    Looping Through the Month

    现在,该继续着手创造那些最重要的日子了。 在monthMetadata(for :)下面添加以下两种方法:

    // 1
    func generateDaysInMonth(for baseDate: Date) -> [Day] {
      // 2
      guard let metadata = try? monthMetadata(for: baseDate) else {
        fatalError("An error occurred when generating the metadata for \(baseDate)")
      }
    
      let numberOfDaysInMonth = metadata.numberOfDays
      let offsetInInitialRow = metadata.firstDayWeekday
      let firstDayOfMonth = metadata.firstDay
    
      // 3
      let 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)
        }
    
      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) 定义一个名为generateDaysInMonth(for :)的方法,该方法接受一个Date并返回一个Days数组。
    • 2) 使用monthMetadata(for :)检索大约一个月所需的元数据。如果此处出现问题,则该应用无法运行。结果,它以fatalError终止。
    • 3) 如果一个月的起始日期不是星期日,则从上个月开始添加最后几天。这样可以避免一个月的第一行出现空白。在这里,您可以创建一个Range <Int>来处理这种情况。例如,如果一个月从星期五开始,则offsetInInitialRow将增加五天来使行平均。然后,您可以使用map(_ :)range转换为[Day]
    • 4) 检查循环中的当前日期是在当前月份内还是在上个月的一部分内。
    • 5) 计算该day与该月第一天的偏移量。如果day在上个月,则该值为负。
    • 6) 调用generateDay(offsetBy:for:isWithinDisplayedMonth :),它会从Date中添加或减去一个偏移量以产生一个新的偏移量,并返回其结果。

    首先,要掌握第四步中发生的事情可能很棘手。下面是一个易于理解的图表:

    牢记这个概念; 您将在下一部分中再次使用它。


    Handling the Last Week of the Month

    与上一节非常相似,如果一个月的最后一天不是在星期六,则必须在日历中添加额外的天数。

    在您刚添加的方法之后,添加以下内容:

    // 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
    }
    

    以下是您刚刚做的事情的细目:

    • 1) 定义一个名为generateStartOfNextMonth(using :)的方法,该方法采用显示月份的第一天,并返回一个Day对象数组。
    • 2) 检索显示月份的最后一天。如果失败,则返回一个空数组。
    • 3) 计算您需要填写日历最后一行的额外天数。例如,如果一个月的最后一天是星期六,则结果为零,并且您返回一个空数组。
    • 4) 如上一节中所述,创建一个Range<Int>1additionalDays的值。然后,将其转换为Days数组。这次,generateDay(offsetBy:for:isWithinDisplayedMonth :)将循环中的当前日期添加到lastDayInMonth中,以生成下个月初的日期。

    最后,您需要将这种方法的结果与上一节中生成的日期结合起来。导航到generateDaysInMonth(for :)并将dayslet更改为var。然后,在return语句之前,添加以下代码行:

    days += generateStartOfNextMonth(using: firstDayOfMonth)
    

    现在,您已经准备好所有日历数据,但是如果看不到这些数据,该怎么办呢? 现在是时候创建要显示的用户界面了。


    Creating the Collection View Cell

    CalendarPicker文件夹中打开CalendarDateCollectionViewCell.swift。 此文件包含样板代码,您将在上面进行扩展。

    在类的顶部,添加以下三个属性:

    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
    }()
    

    selectionBackgroundView是一个红色的圆圈,当用户选择此单元格时(当有空间显示它时),该圆圈会出现。

    numberLabel显示此单元格的月份。

    accessibilityDateFormatter是一个DateFormatter,它将单元格的日期转换为更易于访问的格式。

    接下来,在accessibilityTraits = .button下的初始化程序内,向单元格中添加selectionBackgroundViewnumberLabel

    contentView.addSubview(selectionBackgroundView)
    contentView.addSubview(numberLabel)
    

    接下来,您将为这些视图设置约束。

    1. Setting the Cell’s Constraints

    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
    

    分解一下,您:

    • 1) 根据设备的水平尺寸类别计算宽度和高度。 如果设备水平紧凑,则可以使用单元格的完整大小,同时减去10(最大为60),以确保圆圈不会延伸到单元格边界的边缘。 对于非紧凑型设备,请使用静态45 x 45尺寸。
    • 2) 为number label和选择背景视图设置所有必需的约束,并将选择背景视图的角半径设置为其大小的一半。

    2. Configuring the Cell’s Appearance

    接下来,CalendarDateCollectionViewCell需要参考要显示的Day

    reuseIdentifier下,创建一个day属性:

    var day: Day? {
      didSet {
        guard let day = day else { return }
    
        numberLabel.text = day.number
        accessibilityLabel = accessibilityDateFormatter.string(from: day.date)
      }
    }
    

    设置day后,您将更新numberLabel以反映新的Day。 您还可以将单元格的accessibilityLabel更新为day格式的字符串。 这为所有用户提供了可访问的体验。

    在文件底部,添加以下扩展名:

    // 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
      }
    }
    

    使用上面的代码,您:

    • 1) 定义updateSelectionStatus(),在其中您可以根据当天的选择状态将不同的样式应用于单元格。
    • 2) 添加一个计算的属性,该属性确定屏幕尺寸的宽度是否有限。
    • 3) 添加applySelectedStyle(),当用户选择单元格时,该值将根据屏幕大小应用。
    • 4) 定义applyDefaultStyle(isWithinDisplayedMonth :),它将默认样式应用于单元格。

    最后,请在daydidSet闭包末尾添加以下内容:

    updateSelectionStatus()
    

    CalendarDateCollectionViewCell现在可以准备黄金时间了。


    Preparing the Month View for Data

    打开CalendarPickerViewController.swift。 在Calendar Data Values部分的selectedDate下,添加以下代码:

    private var baseDate: Date {
      didSet {
        days = generateDaysInMonth(for: baseDate)
        collectionView.reloadData()
      }
    }
    
    private lazy var days = generateDaysInMonth(for: baseDate)
    
    private var numberOfWeeksInBaseDate: Int {
      calendar.range(of: .weekOfMonth, in: .month, for: baseDate)?.count ?? 0
    }
    

    这将创建baseDate,该日期保存任务的截止日期。 发生这种情况时,您将生成新的月份数据并重新加载收集视图。 days包含基准日期的月份数据。 初始化CalendarPickerViewController时,将执行days的默认值。 numberOfWeeksInBaseDate表示当前显示的月份中的星期数。

    接下来,在初始化程序中,在self.selectedDate = baseDate下面,为baseDate分配一个值:

    self.baseDate = baseDate
    

    然后,在文件底部,添加以下扩展名:

    // 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
    
        cell.day = day
        return cell
      }
    }
    

    在上面的代码中,您只需实现collection view的数据源,即可从collectionView(_:numberOfItemsInSection :)中返回日单元格的数量,并从days返回每个索引路径的特定单元格。 在collectionView(_:cellForItemAt :)中。

    1. Adding UICollectionViewDelegateFlowLayout Conformance

    设置好基本数据源代理后,您还必须实现集合视图的Flow Layout代理,以定义collection view layout中每个单元格的确切大小。

    通过在文件底部添加以下扩展名来实现此委托:

    // 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)
      }
    }
    

    由于UICollectionViewDelegateFlowLayout实际上是UICollectionViewDelegate的子协议,因此您还可以借此机会实现collectionView(_:didSelectItemAt :)来定义当用户选择日单元格时发生的情况。

    就像您在collectionView(_:cellForItemAt :)中所做的一样,您在collectionView(_:didSelectItemAt :)中要做的第一件事就是访问单元格的Day。然后,用选定的日期调用selectedDateChanged闭包。最后,您关闭日历选择器。

    collectionView(_:layout:sizeForItemAt :)中,您可以计算每个collection view cell的大小。宽度是collection view的宽度,除以7(一周中的天数)。高度是集合视图的高度除以一个月中的星期数。

    注意:您可能想知道为什么widthheightInt而不是在collectionView(_:layout:sizeForItemAt :)中保留为CGFloat。这是因为永远不能保证浮点类型的算术精度。因此,值容易出现舍入错误,这可能会在代码中产生不确定的结果。 IntCGFloat舍入到最接近的整数,在这种情况下,这样做更安全。

    如果您想了解更多信息,请查看: What Every Computer Scientist Should Know About Floating-Point Arithmetic

    2. Presenting the Calendar

    您几乎已经准备好查看自定义日历!仅剩两个快速步骤。

    CalendarPickerViewController.swift中,在viewDidLoad()底部,添加以下代码:

    collectionView.register(
      CalendarDateCollectionViewCell.self, 
      forCellWithReuseIdentifier: CalendarDateCollectionViewCell.reuseIdentifier
    )
    
    collectionView.dataSource = self
    collectionView.delegate = self
    

    这会将自定义单元格注册到collection view,并设置数据源和委托。

    最后,在viewDidLoad()下,添加以下方法:

    override func viewWillTransition(
      to size: CGSize, 
      with coordinator: UIViewControllerTransitionCoordinator
    ) {
      super.viewWillTransition(to: size, with: coordinator)
      collectionView.reloadData()
    }
    

    当设备旋转或在iPad上进入Split View时,这可使collection view重新计算其布局。

    现在是时候开始查看您的日历了。 构建并运行!

    看一下:您对闪亮的新日历控件的第一印象!

    看起来不错,但还没完成。 现在该添加页眉和页脚了。


    Adding the Header and Footer

    您可能已经注意到CalendarPickerHeaderView.swiftCalendarPickerFooterView.swift已在项目中。 但是,它们没有集成到CalendarPickerViewController中。 您现在就要做。

    CalendarPickerViewController.swift内部以及collectionView部分中的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
      })
    

    这些视图分别存储闭包以响应其中发生的UI事件。 当用户点击Exit按钮时,header view将调用闭包。

    用户点击PreviousNext按钮时,便会出现页脚视图的闭包。 结果,这些闭包中的代码相应地递增或递减baseDate

    接下来,将这行代码添加到baseDatedidSet块的末尾和viewDidLoad()的底部:

    headerView.baseDate = baseDate
    

    这将更新headerViewbaseDate

    最后一步是进入viewDidLoad()并将页眉和页脚视图添加到层次结构中。

    view.addSubview(collectionView)下面,添加以下代码:

    view.addSubview(headerView) 
    view.addSubview(footerView)
    

    然后,在对NSLayoutConstraint.activate(_ :)的现有调用之前添加以下约束:

    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)
    ])
    

    页眉视图显示在集合视图上方,高度固定为85点,而页脚视图显示在底部,高度固定为60点。

    现在该是您来之不易的骄傲时刻。 构建并运行!

    太棒了! 您现在有了一个功能强大的日历选择器。 再次选择日期时,您的用户将永远不会挣扎。

    如果您想进一步了解Foundation中的Calendar API,可以查看Apple’s Calendar documentation page

    格式化日期以显示给用户可能很棘手。 请访问nsdateformatter.com,它将帮助您记住所有不同的格式字符串。

    后记

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

    相关文章

      网友评论

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

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