版本记录
版本号 | 时间 |
---|---|
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
typealias Snapshot = NSDiffableDataSourceSnapshot
// 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
的简单示例的简单示例,感兴趣的给个赞或者关注~~~