版本记录
版本号 | 时间 |
---|---|
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 Foundation
是Foundation
的一部分,它使您可以更改和访问有关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>
从1
到additionalDays
的值。然后,将其转换为Days
数组。这次,generateDay(offsetBy:for:isWithinDisplayedMonth :)
将循环中的当前日期添加到lastDayInMonth
中,以生成下个月初的日期。
最后,您需要将这种方法的结果与上一节中生成的日期结合起来。导航到generateDaysInMonth(for :)
并将days
从let
更改为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
下的初始化程序内,向单元格中添加selectionBackgroundView
和numberLabel
:
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 :)
,它将默认样式应用于单元格。
最后,请在day
的didSet
闭包末尾添加以下内容:
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(一周中的天数)。高度是集合视图的高度除以一个月中的星期数。
注意:您可能想知道为什么
width
和height
是Int
而不是在collectionView(_:layout:sizeForItemAt :)
中保留为CGFloat
。这是因为永远不能保证浮点类型的算术精度。因此,值容易出现舍入错误,这可能会在代码中产生不确定的结果。Int
将CGFloat
舍入到最接近的整数,在这种情况下,这样做更安全。如果您想了解更多信息,请查看: 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.swift
和CalendarPickerFooterView.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
将调用闭包。
用户点击Previous
或Next
按钮时,便会出现页脚视图的闭包。 结果,这些闭包中的代码相应地递增或递减baseDate
。
接下来,将这行代码添加到baseDate
的didSet
块的末尾和viewDidLoad()
的底部:
headerView.baseDate = baseDate
这将更新headerView
的baseDate
。
最后一步是进入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
的简单示例的简单示例,感兴趣的给个赞或者关注~~~
网友评论