Advance UIKit rewrite with AMap integration and core UI modules.
Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -6,39 +6,191 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
// MARK: - Diffable 标识
|
||||
|
||||
/// 统计页 section 标识,区分空状态、周期切换、汇总与每日明细。
|
||||
private enum StatisticsSection: Hashable {
|
||||
case empty
|
||||
case period
|
||||
case summary
|
||||
case daily
|
||||
}
|
||||
|
||||
/// 统计页 item 标识,携带展示数据以保证 diff 后 Cell 内容同步更新。
|
||||
private enum StatisticsItem: Hashable {
|
||||
case emptyContext
|
||||
case period(StatisticsPeriod)
|
||||
case summary(StatisticsSummaryResponse)
|
||||
case dailyEmpty
|
||||
case daily(StatisticsDailyItem)
|
||||
}
|
||||
|
||||
/// 数据 Tab 根页面,展示订单统计汇总和每日明细。
|
||||
final class StatisticsViewController: UIViewController {
|
||||
|
||||
private let viewModel = StatisticsViewModel()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .grouped)
|
||||
table.backgroundColor = AppDesignUIKit.pageBackground
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
table.register(StatisticsSummaryCell.self, forCellReuseIdentifier: StatisticsSummaryCell.reuseID)
|
||||
table.register(StatisticsDailyCell.self, forCellReuseIdentifier: StatisticsDailyCell.reuseID)
|
||||
table.register(StatisticsPeriodCell.self, forCellReuseIdentifier: StatisticsPeriodCell.reuseID)
|
||||
return table
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
let layout = makeCollectionLayout()
|
||||
let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collection.backgroundColor = AppDesignUIKit.pageBackground
|
||||
collection.delegate = self
|
||||
collection.register(StatisticsPeriodCell.self, forCellWithReuseIdentifier: StatisticsPeriodCell.reuseID)
|
||||
collection.register(StatisticsSummaryCell.self, forCellWithReuseIdentifier: StatisticsSummaryCell.reuseID)
|
||||
collection.register(StatisticsDailyCell.self, forCellWithReuseIdentifier: StatisticsDailyCell.reuseID)
|
||||
collection.register(StatisticsEmptyCell.self, forCellWithReuseIdentifier: StatisticsEmptyCell.reuseID)
|
||||
collection.register(
|
||||
CollectionSectionHeaderView.self,
|
||||
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
|
||||
withReuseIdentifier: CollectionSectionHeaderView.reuseID
|
||||
)
|
||||
return collection
|
||||
}()
|
||||
|
||||
private lazy var refreshControl = UIRefreshControl()
|
||||
|
||||
/// Diffable 数据源,驱动多 section 列表与分页插入动画。
|
||||
private var dataSource: UICollectionViewDiffableDataSource<StatisticsSection, StatisticsItem>!
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "数据"
|
||||
view.backgroundColor = AppDesignUIKit.pageBackground
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
configureDataSource()
|
||||
view.addSubview(collectionView)
|
||||
collectionView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
|
||||
tableView.refreshControl = refreshControl
|
||||
collectionView.refreshControl = refreshControl
|
||||
|
||||
viewModel.onChange = { [weak self] in self?.tableView.reloadData() }
|
||||
viewModel.onChange = { [weak self] in self?.applySnapshot() }
|
||||
appServices.accountContext.onChange = { [weak self] in Task { await self?.reload(showLoading: true) } }
|
||||
appServices.permissionContext.onChange = { [weak self] in Task { await self?.reload(showLoading: true) } }
|
||||
applySnapshot(animated: false)
|
||||
Task { await reload(showLoading: true) }
|
||||
}
|
||||
|
||||
/// 构建 Compositional Layout,按 section 类型分配全宽卡片布局。
|
||||
private func makeCollectionLayout() -> UICollectionViewCompositionalLayout {
|
||||
UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in
|
||||
guard let self else { return CollectionDiffableLayout.fullWidthSection() }
|
||||
let snapshot = self.dataSource?.snapshot()
|
||||
let section = snapshot?.sectionIdentifiers[safe: sectionIndex]
|
||||
switch section {
|
||||
case .empty:
|
||||
return CollectionDiffableLayout.fullWidthSection(estimatedHeight: 360)
|
||||
case .period:
|
||||
return CollectionDiffableLayout.fullWidthSection(estimatedHeight: 56)
|
||||
case .summary:
|
||||
return CollectionDiffableLayout.fullWidthSection(estimatedHeight: 280)
|
||||
case .daily:
|
||||
return CollectionDiffableLayout.addHeader(
|
||||
to: CollectionDiffableLayout.fullWidthSection(estimatedHeight: 52),
|
||||
height: 36
|
||||
)
|
||||
case .none:
|
||||
return CollectionDiffableLayout.fullWidthSection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册 Diffable 数据源与 Cell 配置闭包。
|
||||
private func configureDataSource() {
|
||||
dataSource = UICollectionViewDiffableDataSource<StatisticsSection, StatisticsItem>(
|
||||
collectionView: collectionView
|
||||
) { [weak self] collectionView, indexPath, item in
|
||||
guard let self else { return UICollectionViewCell() }
|
||||
switch item {
|
||||
case .emptyContext:
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: StatisticsEmptyCell.reuseID,
|
||||
for: indexPath
|
||||
) as! StatisticsEmptyCell
|
||||
let empty = self.makeEmptyStateView(
|
||||
title: "缺少经营上下文",
|
||||
message: "请先在首页选择景区后查看数据看板。",
|
||||
systemImage: "chart.bar.doc.horizontal"
|
||||
)
|
||||
cell.setHostedView(empty, height: 360)
|
||||
return cell
|
||||
case .period(let selectedPeriod):
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: StatisticsPeriodCell.reuseID,
|
||||
for: indexPath
|
||||
) as! StatisticsPeriodCell
|
||||
cell.configure(selectedPeriod: selectedPeriod) { [weak self] period in
|
||||
self?.selectPeriod(period)
|
||||
}
|
||||
return cell
|
||||
case .summary(let summary):
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: StatisticsSummaryCell.reuseID,
|
||||
for: indexPath
|
||||
) as! StatisticsSummaryCell
|
||||
cell.configure(
|
||||
periodText: self.viewModel.selectedPeriod.selectedTimeText,
|
||||
summary: summary,
|
||||
amountText: self.amountText
|
||||
)
|
||||
return cell
|
||||
case .dailyEmpty:
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: StatisticsEmptyCell.reuseID,
|
||||
for: indexPath
|
||||
) as! StatisticsEmptyCell
|
||||
cell.configurePlainText("暂无数据", height: 52)
|
||||
return cell
|
||||
case .daily(let dailyItem):
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: StatisticsDailyCell.reuseID,
|
||||
for: indexPath
|
||||
) as! StatisticsDailyCell
|
||||
cell.configure(item: dailyItem, amountText: self.amountText)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
|
||||
guard kind == UICollectionView.elementKindSectionHeader,
|
||||
let section = self.dataSource.snapshot().sectionIdentifiers[safe: indexPath.section],
|
||||
section == .daily,
|
||||
let header = collectionView.dequeueReusableSupplementaryView(
|
||||
ofKind: kind,
|
||||
withReuseIdentifier: CollectionSectionHeaderView.reuseID,
|
||||
for: indexPath
|
||||
) as? CollectionSectionHeaderView
|
||||
else { return nil }
|
||||
header.configure(title: "每日明细")
|
||||
return header
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据 ViewModel 状态构建 snapshot 并应用 diff 更新。
|
||||
private func applySnapshot(animated: Bool = true) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<StatisticsSection, StatisticsItem>()
|
||||
|
||||
if currentScenicId == nil {
|
||||
snapshot.appendSections([.empty])
|
||||
snapshot.appendItems([.emptyContext], toSection: .empty)
|
||||
} else {
|
||||
snapshot.appendSections([.period, .summary, .daily])
|
||||
snapshot.appendItems([.period(viewModel.selectedPeriod)], toSection: .period)
|
||||
snapshot.appendItems([.summary(viewModel.summary)], toSection: .summary)
|
||||
|
||||
if viewModel.loading, viewModel.dailyItems.isEmpty {
|
||||
// 首次加载中不展示占位行,避免闪烁。
|
||||
} else if viewModel.dailyItems.isEmpty {
|
||||
snapshot.appendItems([.dailyEmpty], toSection: .daily)
|
||||
} else {
|
||||
let items = viewModel.dailyItems.map { StatisticsItem.daily($0) }
|
||||
snapshot.appendItems(items, toSection: .daily)
|
||||
}
|
||||
}
|
||||
|
||||
dataSource.apply(snapshot, animatingDifferences: animated)
|
||||
}
|
||||
|
||||
/// 下拉刷新触发。
|
||||
@objc private func refreshPulled() {
|
||||
Task {
|
||||
await reload(showLoading: false)
|
||||
@ -49,9 +201,10 @@ final class StatisticsViewController: UIViewController {
|
||||
private var currentScenicId: Int? { appServices.accountContext.currentScenic?.id }
|
||||
private var currentRoleId: Int? { appServices.permissionContext.currentRole?.id }
|
||||
|
||||
/// 重新加载汇总与第一页日数据。
|
||||
private func reload(showLoading: Bool) async {
|
||||
guard currentScenicId != nil else {
|
||||
tableView.reloadData()
|
||||
applySnapshot()
|
||||
return
|
||||
}
|
||||
do {
|
||||
@ -68,6 +221,7 @@ final class StatisticsViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换统计周期并重新加载数据。
|
||||
private func selectPeriod(_ period: StatisticsPeriod) {
|
||||
Task {
|
||||
do {
|
||||
@ -85,6 +239,7 @@ final class StatisticsViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载每日明细下一页。
|
||||
private func loadMore() async {
|
||||
do {
|
||||
try await viewModel.loadMore(
|
||||
@ -97,85 +252,37 @@ final class StatisticsViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 格式化金额展示文本。
|
||||
private func amountText(_ value: Double) -> String {
|
||||
"¥\(String(format: "%.2f", value))"
|
||||
}
|
||||
}
|
||||
|
||||
extension StatisticsViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
currentScenicId == nil ? 1 : 3
|
||||
}
|
||||
// MARK: - UICollectionViewDelegate
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if currentScenicId == nil { return 1 }
|
||||
switch section {
|
||||
case 0: return 1
|
||||
case 1: return 1
|
||||
default:
|
||||
if viewModel.loading && viewModel.dailyItems.isEmpty { return 0 }
|
||||
return max(viewModel.dailyItems.count, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
if currentScenicId == nil {
|
||||
let cell = UITableViewCell()
|
||||
cell.selectionStyle = .none
|
||||
cell.backgroundColor = .clear
|
||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
let empty = makeEmptyStateView(title: "缺少经营上下文", message: "请先在首页选择景区后查看数据看板。", systemImage: "chart.bar.doc.horizontal")
|
||||
cell.contentView.addSubview(empty)
|
||||
empty.snp.makeConstraints { make in make.edges.equalToSuperview(); make.height.equalTo(360) }
|
||||
return cell
|
||||
}
|
||||
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: StatisticsPeriodCell.reuseID, for: indexPath) as! StatisticsPeriodCell
|
||||
cell.configure(selectedPeriod: viewModel.selectedPeriod) { [weak self] period in
|
||||
self?.selectPeriod(period)
|
||||
}
|
||||
return cell
|
||||
case 1:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: StatisticsSummaryCell.reuseID, for: indexPath) as! StatisticsSummaryCell
|
||||
cell.configure(
|
||||
periodText: viewModel.selectedPeriod.selectedTimeText,
|
||||
summary: viewModel.summary,
|
||||
amountText: amountText
|
||||
)
|
||||
return cell
|
||||
default:
|
||||
if viewModel.dailyItems.isEmpty {
|
||||
let cell = UITableViewCell()
|
||||
cell.textLabel?.text = "暂无数据"
|
||||
cell.selectionStyle = .none
|
||||
return cell
|
||||
}
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: StatisticsDailyCell.reuseID, for: indexPath) as! StatisticsDailyCell
|
||||
cell.configure(item: viewModel.dailyItems[indexPath.row], amountText: amountText)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
section == 2 ? "每日明细" : nil
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
guard indexPath.section == 2, indexPath.row == viewModel.dailyItems.count - 1 else { return }
|
||||
extension StatisticsViewController: UICollectionViewDelegate {
|
||||
/// 最后一项日明细即将展示时触发分页加载。
|
||||
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||
case .daily(let dailyItem) = item,
|
||||
dailyItem.date == viewModel.dailyItems.last?.date
|
||||
else { return }
|
||||
Task { await loadMore() }
|
||||
}
|
||||
}
|
||||
|
||||
private final class StatisticsPeriodCell: UITableViewCell {
|
||||
// MARK: - Cells
|
||||
|
||||
/// 统计周期切换 Cell,横向展示快捷时间段按钮。
|
||||
private final class StatisticsPeriodCell: UICollectionViewCell {
|
||||
static let reuseID = "StatisticsPeriodCell"
|
||||
private var onSelect: ((StatisticsPeriod) -> Void)?
|
||||
private let stack = UIStackView()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
/// 初始化实例。
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
stack.axis = .horizontal
|
||||
stack.spacing = 8
|
||||
stack.distribution = .fillEqually
|
||||
@ -186,6 +293,7 @@ private final class StatisticsPeriodCell: UITableViewCell {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 配置展示内容。
|
||||
func configure(selectedPeriod: StatisticsPeriod, onSelect: @escaping (StatisticsPeriod) -> Void) {
|
||||
self.onSelect = onSelect
|
||||
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
@ -202,19 +310,22 @@ private final class StatisticsPeriodCell: UITableViewCell {
|
||||
}
|
||||
}
|
||||
|
||||
/// 点击周期按钮后回调选中项。
|
||||
@objc private func periodTapped(_ sender: UIButton) {
|
||||
let period = StatisticsPeriod.allCases[sender.tag]
|
||||
onSelect?(period)
|
||||
}
|
||||
}
|
||||
|
||||
private final class StatisticsSummaryCell: UITableViewCell {
|
||||
/// 统计汇总 Cell,展示订单金额、数量、客单价等指标卡片。
|
||||
private final class StatisticsSummaryCell: UICollectionViewCell {
|
||||
static let reuseID = "StatisticsSummaryCell"
|
||||
private let stack = UIStackView()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
/// 初始化实例。
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 8
|
||||
contentView.addSubview(stack)
|
||||
@ -224,6 +335,7 @@ private final class StatisticsSummaryCell: UITableViewCell {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 配置展示内容。
|
||||
func configure(periodText: String, summary: StatisticsSummaryResponse, amountText: (Double) -> String) {
|
||||
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
let dateLabel = UILabel()
|
||||
@ -249,6 +361,7 @@ private final class StatisticsSummaryCell: UITableViewCell {
|
||||
stack.addArrangedSubview(row2)
|
||||
}
|
||||
|
||||
/// 创建统计摘要卡片视图。
|
||||
private func summaryCard(_ title: String, _ value: String, _ color: UIColor) -> UIView {
|
||||
let card = UIView()
|
||||
card.backgroundColor = color.withAlphaComponent(0.08)
|
||||
@ -270,21 +383,94 @@ private final class StatisticsSummaryCell: UITableViewCell {
|
||||
}
|
||||
}
|
||||
|
||||
private final class StatisticsDailyCell: UITableViewCell {
|
||||
/// 每日统计明细 Cell,展示日期、订单数与实收金额。
|
||||
private final class StatisticsDailyCell: UICollectionViewCell {
|
||||
static let reuseID = "StatisticsDailyCell"
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
private let titleLabel = UILabel()
|
||||
private let detailLabel = UILabel()
|
||||
|
||||
/// 初始化实例。
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .secondarySystemGroupedBackground
|
||||
contentView.backgroundColor = .secondarySystemGroupedBackground
|
||||
contentView.layer.cornerRadius = 8
|
||||
contentView.clipsToBounds = true
|
||||
|
||||
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: .semibold)
|
||||
detailLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||||
detailLabel.textColor = AppDesignUIKit.textSecondary
|
||||
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(detailLabel)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.top.equalToSuperview().inset(12)
|
||||
}
|
||||
detailLabel.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalTo(titleLabel)
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(4)
|
||||
make.bottom.equalToSuperview().inset(12)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 配置展示内容。
|
||||
func configure(item: StatisticsDailyItem, amountText: (Double) -> String) {
|
||||
textLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: .semibold)
|
||||
detailTextLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||||
textLabel?.text = item.date
|
||||
detailTextLabel?.text = "\(item.orderCount)单 · 实收 \(amountText(item.receivedValue))"
|
||||
titleLabel.text = item.date
|
||||
detailLabel.text = "\(item.orderCount)单 · 实收 \(amountText(item.receivedValue))"
|
||||
}
|
||||
}
|
||||
|
||||
/// 空状态 Cell,用于缺少景区上下文或日明细为空时的占位展示。
|
||||
private final class StatisticsEmptyCell: UICollectionViewCell {
|
||||
static let reuseID = "StatisticsEmptyCell"
|
||||
|
||||
private var heightConstraint: Constraint?
|
||||
|
||||
/// 初始化实例。
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 嵌入外部构建的空状态视图并固定高度。
|
||||
func setHostedView(_ view: UIView, height: CGFloat) {
|
||||
contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
heightConstraint?.deactivate()
|
||||
contentView.addSubview(view)
|
||||
view.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
heightConstraint = make.height.equalTo(height).constraint
|
||||
}
|
||||
}
|
||||
|
||||
/// 配置纯文案占位,用于日明细为空场景。
|
||||
func configurePlainText(_ title: String, height: CGFloat) {
|
||||
contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
heightConstraint?.deactivate()
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
label.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
label.textColor = AppDesignUIKit.textSecondary
|
||||
label.textAlignment = .center
|
||||
contentView.addSubview(label)
|
||||
label.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(16)
|
||||
heightConstraint = make.height.equalTo(height).constraint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全下标,避免 section 越界。
|
||||
private extension Array {
|
||||
subscript(safe index: Int) -> Element? {
|
||||
indices.contains(index) ? self[index] : nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user