对齐 Android 样片模块布局与加载状态,补充共享 UI 组件与图片资源,并增强列表解析容错与单元测试覆盖。 Co-authored-by: Cursor <cursoragent@cursor.com>
574 lines
22 KiB
Swift
574 lines
22 KiB
Swift
//
|
||
// PunchPointDetailViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import Kingfisher
|
||
import Photos
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
/// 打卡点详情页,对齐 Android `PunchPointDetailScreen`。
|
||
final class PunchPointDetailViewController: BaseViewController {
|
||
private let viewModel: PunchPointDetailViewModel
|
||
private let api: any PunchPointAPIProtocol
|
||
|
||
private let mapView = PunchPointMapView()
|
||
private let zoomStack = UIStackView()
|
||
private let zoomInButton = PunchPointMapControlButton(symbol: "plus")
|
||
private let zoomOutButton = PunchPointMapControlButton(symbol: "minus")
|
||
private let locateButton = PunchPointMapControlButton(symbol: "location.fill")
|
||
private let cardView = UIView()
|
||
private let scrollView = UIScrollView()
|
||
private let contentStack = UIStackView()
|
||
private let bottomBar = UIView()
|
||
private let editButton = UIButton(type: .system)
|
||
private let emptyContainer = UIStackView()
|
||
private let emptyLabel = UILabel()
|
||
private let retryButton = UIButton(type: .system)
|
||
|
||
var onChanged: (() -> Void)?
|
||
|
||
/// 初始化打卡点详情页。
|
||
init(
|
||
punchPointId: Int64,
|
||
viewModel: PunchPointDetailViewModel? = nil,
|
||
api: (any PunchPointAPIProtocol)? = nil
|
||
) {
|
||
self.viewModel = viewModel ?? PunchPointDetailViewModel(punchPointId: punchPointId)
|
||
self.api = api ?? NetworkServices.shared.punchPointAPI
|
||
super.init(nibName: nil, bundle: nil)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func setupNavigationBar() {
|
||
title = "打卡点详情"
|
||
}
|
||
|
||
override func setupUI() {
|
||
view.backgroundColor = AppColor.pageBackground
|
||
|
||
zoomStack.axis = .vertical
|
||
zoomStack.spacing = 0
|
||
zoomStack.addArrangedSubview(zoomInButton)
|
||
zoomStack.addArrangedSubview(zoomOutButton)
|
||
cardView.backgroundColor = .white
|
||
cardView.layer.cornerRadius = 12
|
||
cardView.clipsToBounds = true
|
||
scrollView.showsVerticalScrollIndicator = false
|
||
contentStack.axis = .vertical
|
||
contentStack.spacing = 12
|
||
bottomBar.backgroundColor = .white
|
||
editButton.setTitle("去编辑", for: .normal)
|
||
editButton.setTitleColor(.white, for: .normal)
|
||
editButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||
editButton.backgroundColor = AppColor.primary
|
||
editButton.layer.cornerRadius = 12
|
||
|
||
emptyContainer.axis = .vertical
|
||
emptyContainer.alignment = .center
|
||
emptyContainer.spacing = 12
|
||
emptyLabel.text = "暂未获取到打卡点信息"
|
||
emptyLabel.textColor = AppColor.textSecondary
|
||
emptyLabel.font = .systemFont(ofSize: 15)
|
||
retryButton.setTitle("重新加载", for: .normal)
|
||
retryButton.setTitleColor(.white, for: .normal)
|
||
retryButton.backgroundColor = AppColor.primary
|
||
retryButton.layer.cornerRadius = 8
|
||
emptyContainer.addArrangedSubview(emptyLabel)
|
||
emptyContainer.addArrangedSubview(retryButton)
|
||
emptyContainer.isHidden = true
|
||
|
||
view.addSubview(mapView)
|
||
view.addSubview(zoomStack)
|
||
view.addSubview(locateButton)
|
||
view.addSubview(cardView)
|
||
cardView.addSubview(scrollView)
|
||
scrollView.addSubview(contentStack)
|
||
view.addSubview(bottomBar)
|
||
bottomBar.addSubview(editButton)
|
||
view.addSubview(emptyContainer)
|
||
}
|
||
|
||
override func setupConstraints() {
|
||
mapView.snp.makeConstraints { make in
|
||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||
make.leading.trailing.equalToSuperview()
|
||
make.height.equalToSuperview().multipliedBy(0.5)
|
||
}
|
||
zoomStack.snp.makeConstraints { make in
|
||
make.trailing.equalToSuperview().inset(16)
|
||
make.bottom.equalTo(cardView.snp.top).offset(-16)
|
||
}
|
||
locateButton.snp.makeConstraints { make in
|
||
make.trailing.equalToSuperview().inset(16)
|
||
make.bottom.equalTo(zoomStack.snp.top).offset(-12)
|
||
make.size.equalTo(40)
|
||
}
|
||
zoomInButton.snp.makeConstraints { make in
|
||
make.size.equalTo(40)
|
||
}
|
||
zoomOutButton.snp.makeConstraints { make in
|
||
make.size.equalTo(40)
|
||
}
|
||
bottomBar.snp.makeConstraints { make in
|
||
make.leading.trailing.bottom.equalToSuperview()
|
||
}
|
||
editButton.snp.makeConstraints { make in
|
||
make.top.equalToSuperview().offset(16)
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
make.height.equalTo(48)
|
||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-20)
|
||
}
|
||
cardView.snp.makeConstraints { make in
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
make.bottom.equalTo(bottomBar.snp.top)
|
||
make.height.lessThanOrEqualToSuperview().multipliedBy(0.45)
|
||
make.top.greaterThanOrEqualTo(mapView.snp.top).offset(60)
|
||
}
|
||
scrollView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
}
|
||
contentStack.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 16, left: 12, bottom: 12, right: 12))
|
||
make.width.equalTo(scrollView.snp.width).offset(-24)
|
||
}
|
||
emptyContainer.snp.makeConstraints { make in
|
||
make.center.equalToSuperview()
|
||
}
|
||
retryButton.snp.makeConstraints { make in
|
||
make.width.equalTo(118)
|
||
make.height.equalTo(40)
|
||
}
|
||
}
|
||
|
||
override func bindActions() {
|
||
viewModel.onStateChange = { [weak self] in
|
||
Task { @MainActor in self?.applyViewModel() }
|
||
}
|
||
viewModel.onShowMessage = { [weak self] message in
|
||
Task { @MainActor in self?.showToast(message) }
|
||
}
|
||
zoomInButton.addTarget(self, action: #selector(zoomInTapped), for: .touchUpInside)
|
||
zoomOutButton.addTarget(self, action: #selector(zoomOutTapped), for: .touchUpInside)
|
||
locateButton.addTarget(self, action: #selector(locateTapped), for: .touchUpInside)
|
||
retryButton.addTarget(self, action: #selector(retryTapped), for: .touchUpInside)
|
||
editButton.addTarget(self, action: #selector(editTapped), for: .touchUpInside)
|
||
}
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
Task { await viewModel.load(api: api) }
|
||
}
|
||
|
||
@MainActor
|
||
private func applyViewModel() {
|
||
viewModel.isLoading && viewModel.detail == nil ? showLoading() : hideLoading()
|
||
emptyContainer.isHidden = viewModel.detail != nil || viewModel.isLoading
|
||
guard let detail = viewModel.detail else {
|
||
cardView.isHidden = true
|
||
bottomBar.isHidden = true
|
||
return
|
||
}
|
||
cardView.isHidden = false
|
||
bottomBar.isHidden = false
|
||
if let region = detail.region {
|
||
let coordinate = CLLocationCoordinate2D(latitude: region.lat, longitude: region.lot)
|
||
mapView.updateMarker(coordinate: coordinate)
|
||
mapView.setCenter(coordinate, zoomLevel: 16)
|
||
}
|
||
rebuildContent(detail)
|
||
}
|
||
|
||
private func rebuildContent(_ detail: PunchPointDetail) {
|
||
contentStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||
contentStack.addArrangedSubview(PunchPointReadOnlyField(title: "*打卡点名称", value: detail.name))
|
||
let coordinateText = detail.region.map { "\($0.lat),\($0.lot)" } ?? "--"
|
||
contentStack.addArrangedSubview(PunchPointReadOnlyField(title: "*打卡点坐标", value: coordinateText))
|
||
contentStack.addArrangedSubview(PunchPointReadOnlyField(title: "*打卡点地址", value: detail.displayAddress.nonEmptyTrimmed ?? "--"))
|
||
if let description = detail.description?.nonEmptyTrimmed {
|
||
contentStack.addArrangedSubview(PunchPointReadOnlyField(title: "打卡点描述", value: description, multiline: true))
|
||
}
|
||
contentStack.addArrangedSubview(makeImageSection(detail.guideImages))
|
||
contentStack.addArrangedSubview(PunchPointRowField(title: "负责人", value: detail.creator?.nonEmptyTrimmed ?? "--"))
|
||
contentStack.addArrangedSubview(PunchPointDivider())
|
||
contentStack.addArrangedSubview(PunchPointRowField(title: "负责人手机号", value: PunchPointDisplayFormatter.maskedPhone(detail.creatorPhone)))
|
||
contentStack.addArrangedSubview(PunchPointDivider())
|
||
contentStack.addArrangedSubview(makeStatusRow(detail))
|
||
contentStack.addArrangedSubview(PunchPointDivider())
|
||
contentStack.addArrangedSubview(PunchPointRowField(title: "审核人", value: detail.auditor?.nonEmptyTrimmed ?? "--"))
|
||
contentStack.addArrangedSubview(PunchPointDivider())
|
||
contentStack.addArrangedSubview(PunchPointRowField(title: "审核时间", value: detail.auditTime?.nonEmptyTrimmed ?? "--"))
|
||
contentStack.addArrangedSubview(PunchPointDivider())
|
||
contentStack.addArrangedSubview(PunchPointRowField(title: "审核备注", value: detail.auditRemark?.nonEmptyTrimmed ?? "--"))
|
||
contentStack.addArrangedSubview(makeQRSection(detail.mpQrcode))
|
||
}
|
||
|
||
private func makeImageSection(_ urls: [String]) -> UIView {
|
||
let section = UIStackView()
|
||
section.axis = .vertical
|
||
section.spacing = 8
|
||
let title = UILabel()
|
||
title.attributedText = requiredTitle("*上传图片 (最多9张)")
|
||
section.addArrangedSubview(title)
|
||
guard !urls.isEmpty else {
|
||
let empty = UILabel()
|
||
empty.text = "暂无图片"
|
||
empty.font = .systemFont(ofSize: 12)
|
||
empty.textColor = AppColor.textSecondary
|
||
section.addArrangedSubview(empty)
|
||
return section
|
||
}
|
||
let scroll = UIScrollView()
|
||
scroll.showsHorizontalScrollIndicator = false
|
||
let row = UIStackView()
|
||
row.axis = .horizontal
|
||
row.spacing = 12
|
||
scroll.addSubview(row)
|
||
row.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
make.height.equalTo(90)
|
||
}
|
||
urls.enumerated().forEach { index, urlText in
|
||
let imageView = UIImageView()
|
||
imageView.contentMode = .scaleAspectFill
|
||
imageView.clipsToBounds = true
|
||
imageView.layer.cornerRadius = 10
|
||
imageView.isUserInteractionEnabled = true
|
||
imageView.tag = index
|
||
if let url = URL(string: urlText) {
|
||
imageView.kf.setImage(with: url)
|
||
}
|
||
imageView.snp.makeConstraints { make in
|
||
make.width.equalTo(120)
|
||
}
|
||
imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imageTapped(_:))))
|
||
row.addArrangedSubview(imageView)
|
||
}
|
||
scroll.snp.makeConstraints { make in
|
||
make.height.equalTo(90)
|
||
}
|
||
section.addArrangedSubview(scroll)
|
||
return section
|
||
}
|
||
|
||
private func makeStatusRow(_ detail: PunchPointDetail) -> UIView {
|
||
let row = UIStackView()
|
||
row.axis = .horizontal
|
||
row.alignment = .center
|
||
row.distribution = .equalSpacing
|
||
let label = UILabel()
|
||
label.text = "审核状态"
|
||
label.font = .systemFont(ofSize: 14)
|
||
label.textColor = UIColor(hex: 0x2E3746)
|
||
let chip = PunchPointStatusChip()
|
||
chip.apply(text: detail.statusLabel.nonEmptyTrimmed ?? "--", status: detail.status)
|
||
row.addArrangedSubview(label)
|
||
row.addArrangedSubview(chip)
|
||
row.isLayoutMarginsRelativeArrangement = true
|
||
row.layoutMargins = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0)
|
||
return row
|
||
}
|
||
|
||
private func makeQRSection(_ qrCode: String?) -> UIView {
|
||
let section = UIStackView()
|
||
section.axis = .vertical
|
||
section.alignment = .center
|
||
section.spacing = 12
|
||
let title = UILabel()
|
||
title.text = "打卡点二维码"
|
||
title.font = .systemFont(ofSize: 14)
|
||
title.textColor = UIColor(hex: 0x2E3746)
|
||
title.textAlignment = .left
|
||
section.addArrangedSubview(title)
|
||
title.snp.makeConstraints { make in
|
||
make.width.equalTo(section.snp.width)
|
||
}
|
||
guard let qrCode = qrCode?.nonEmptyTrimmed else {
|
||
let empty = UILabel()
|
||
empty.text = "暂无二维码"
|
||
empty.font = .systemFont(ofSize: 12)
|
||
empty.textColor = AppColor.textSecondary
|
||
section.addArrangedSubview(empty)
|
||
return section
|
||
}
|
||
let imageView = UIImageView()
|
||
imageView.contentMode = .scaleAspectFill
|
||
imageView.clipsToBounds = true
|
||
imageView.layer.cornerRadius = 16
|
||
if let url = URL(string: qrCode) {
|
||
imageView.kf.setImage(with: url)
|
||
}
|
||
imageView.snp.makeConstraints { make in
|
||
make.size.equalTo(180)
|
||
}
|
||
let download = UIButton(type: .system)
|
||
download.setTitle("下载二维码", for: .normal)
|
||
download.setTitleColor(AppColor.primary, for: .normal)
|
||
download.titleLabel?.font = .systemFont(ofSize: 14)
|
||
download.backgroundColor = AppColor.primaryLight
|
||
download.layer.cornerRadius = 8
|
||
download.snp.makeConstraints { make in
|
||
make.height.equalTo(38)
|
||
make.width.equalTo(128)
|
||
}
|
||
download.addAction(UIAction { [weak self] _ in
|
||
self?.downloadQRCode(qrCode)
|
||
}, for: .touchUpInside)
|
||
section.addArrangedSubview(imageView)
|
||
section.addArrangedSubview(download)
|
||
return section
|
||
}
|
||
|
||
private func downloadQRCode(_ urlText: String) {
|
||
guard let url = URL(string: urlText) else {
|
||
showToast("二维码地址无效")
|
||
return
|
||
}
|
||
Task {
|
||
do {
|
||
let (data, _) = try await URLSession.shared.data(from: url)
|
||
guard let image = UIImage(data: data) else { throw APIError.decodeFailed("二维码图片无效") }
|
||
try await PHPhotoLibrary.shared().performChanges {
|
||
PHAssetChangeRequest.creationRequestForAsset(from: image)
|
||
}
|
||
await MainActor.run { self.showToast("已保存到相册") }
|
||
} catch {
|
||
await MainActor.run { self.showToast(error.localizedDescription.isEmpty ? "保存失败" : error.localizedDescription) }
|
||
}
|
||
}
|
||
}
|
||
|
||
private func requiredTitle(_ text: String) -> NSAttributedString {
|
||
let mutable = NSMutableAttributedString(string: text, attributes: [
|
||
.font: UIFont.systemFont(ofSize: 14, weight: .medium),
|
||
.foregroundColor: UIColor(hex: 0x2E3746),
|
||
])
|
||
if text.hasPrefix("*") {
|
||
mutable.addAttribute(.foregroundColor, value: AppColor.danger, range: NSRange(location: 0, length: 1))
|
||
}
|
||
return mutable
|
||
}
|
||
|
||
@objc private func imageTapped(_ gesture: UITapGestureRecognizer) {
|
||
guard let imageView = gesture.view, let urls = viewModel.detail?.guideImages else { return }
|
||
presentImagePreview(imageURLs: urls, startIndex: imageView.tag)
|
||
}
|
||
|
||
@objc private func zoomInTapped() {
|
||
mapView.zoomIn()
|
||
}
|
||
|
||
@objc private func zoomOutTapped() {
|
||
mapView.zoomOut()
|
||
}
|
||
|
||
@objc private func locateTapped() {
|
||
mapView.centerOnUserLocation()
|
||
}
|
||
|
||
@objc private func retryTapped() {
|
||
Task { await viewModel.load(api: api) }
|
||
}
|
||
|
||
@objc private func editTapped() {
|
||
guard let detail = viewModel.detail else { return }
|
||
let controller = PunchPointFormViewController(mode: .edit(id: detail.id), initialDetail: detail)
|
||
controller.onSubmitSuccess = { [weak self] in
|
||
guard let self else { return }
|
||
self.onChanged?()
|
||
Task { await self.viewModel.load(api: self.api) }
|
||
}
|
||
navigationController?.pushViewController(controller, animated: true)
|
||
}
|
||
}
|
||
|
||
/// 高德地图容器,供打卡点详情与表单复用。
|
||
/// 打卡点地图视图,封装高德地图 marker、缩放、定位与点击选点行为。
|
||
final class PunchPointMapView: UIView, MAMapViewDelegate {
|
||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||
|
||
private let mapView: MAMapView
|
||
private var markerAnnotation: MAPointAnnotation?
|
||
|
||
override init(frame: CGRect) {
|
||
_ = AMapBootstrap.configureIfNeeded()
|
||
mapView = MAMapView(frame: .zero)
|
||
super.init(frame: frame)
|
||
mapView.delegate = self
|
||
mapView.showsUserLocation = true
|
||
mapView.showsCompass = false
|
||
mapView.showsScale = false
|
||
mapView.zoomLevel = 15
|
||
addSubview(mapView)
|
||
mapView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
/// 更新打卡点标记。
|
||
func updateMarker(coordinate: CLLocationCoordinate2D?) {
|
||
if let markerAnnotation {
|
||
mapView.removeAnnotation(markerAnnotation)
|
||
self.markerAnnotation = nil
|
||
}
|
||
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
|
||
let annotation = MAPointAnnotation()
|
||
annotation.coordinate = coordinate
|
||
annotation.title = "打卡点位置"
|
||
markerAnnotation = annotation
|
||
mapView.addAnnotation(annotation)
|
||
}
|
||
|
||
/// 设置地图中心点。
|
||
func setCenter(_ coordinate: CLLocationCoordinate2D, zoomLevel: Double = 15, animated: Bool = true) {
|
||
mapView.setCenter(coordinate, animated: animated)
|
||
mapView.setZoomLevel(zoomLevel, animated: animated)
|
||
}
|
||
|
||
/// 放大地图。
|
||
func zoomIn() {
|
||
mapView.setZoomLevel(mapView.zoomLevel + 1, animated: true)
|
||
}
|
||
|
||
/// 缩小地图。
|
||
func zoomOut() {
|
||
mapView.setZoomLevel(mapView.zoomLevel - 1, animated: true)
|
||
}
|
||
|
||
/// 居中到用户当前位置。
|
||
func centerOnUserLocation() {
|
||
guard let coordinate = mapView.userLocation.location?.coordinate,
|
||
CLLocationCoordinate2DIsValid(coordinate),
|
||
coordinate.latitude != 0 || coordinate.longitude != 0 else { return }
|
||
setCenter(coordinate)
|
||
}
|
||
|
||
func mapView(_ mapView: MAMapView!, didSingleTappedAt coordinate: CLLocationCoordinate2D) {
|
||
onMapTap?(coordinate)
|
||
}
|
||
}
|
||
|
||
/// 打卡点地图控制按钮。
|
||
/// 地图悬浮控制按钮。
|
||
final class PunchPointMapControlButton: UIButton {
|
||
init(symbol: String) {
|
||
super.init(frame: .zero)
|
||
setImage(UIImage(systemName: symbol), for: .normal)
|
||
tintColor = AppColor.textPrimary
|
||
backgroundColor = .white
|
||
layer.cornerRadius = 12
|
||
clipsToBounds = true
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
}
|
||
|
||
/// 打卡点只读字段。
|
||
/// 详情页只读字段块。
|
||
final class PunchPointReadOnlyField: UIView {
|
||
init(title: String, value: String, multiline: Bool = false) {
|
||
super.init(frame: .zero)
|
||
let stack = UIStackView()
|
||
stack.axis = .vertical
|
||
stack.spacing = 8
|
||
let titleLabel = UILabel()
|
||
titleLabel.attributedText = Self.requiredTitle(title)
|
||
let valueLabel = UILabel()
|
||
valueLabel.text = value.nonEmptyTrimmed ?? "--"
|
||
valueLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||
valueLabel.textColor = AppColor.textPrimary
|
||
valueLabel.numberOfLines = multiline ? 0 : 2
|
||
stack.addArrangedSubview(titleLabel)
|
||
stack.addArrangedSubview(valueLabel)
|
||
addSubview(stack)
|
||
stack.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
private static func requiredTitle(_ text: String) -> NSAttributedString {
|
||
let mutable = NSMutableAttributedString(string: text, attributes: [
|
||
.font: UIFont.systemFont(ofSize: 14, weight: .medium),
|
||
.foregroundColor: UIColor(hex: 0x2E3746),
|
||
])
|
||
if text.hasPrefix("*") {
|
||
mutable.addAttribute(.foregroundColor, value: AppColor.danger, range: NSRange(location: 0, length: 1))
|
||
}
|
||
return mutable
|
||
}
|
||
}
|
||
|
||
/// 打卡点左右字段。
|
||
/// 详情页横向键值字段。
|
||
final class PunchPointRowField: UIView {
|
||
init(title: String, value: String) {
|
||
super.init(frame: .zero)
|
||
let titleLabel = UILabel()
|
||
titleLabel.text = title
|
||
titleLabel.font = .systemFont(ofSize: 14)
|
||
titleLabel.textColor = UIColor(hex: 0x2E3746)
|
||
let valueLabel = UILabel()
|
||
valueLabel.text = value.nonEmptyTrimmed ?? "--"
|
||
valueLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||
valueLabel.textColor = .black
|
||
valueLabel.textAlignment = .right
|
||
valueLabel.numberOfLines = 2
|
||
addSubview(titleLabel)
|
||
addSubview(valueLabel)
|
||
titleLabel.snp.makeConstraints { make in
|
||
make.leading.top.bottom.equalToSuperview().inset(UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0))
|
||
}
|
||
valueLabel.snp.makeConstraints { make in
|
||
make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(12)
|
||
make.trailing.centerY.equalToSuperview()
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
}
|
||
|
||
/// 打卡点详情分割线。
|
||
/// 详情卡片内分割线。
|
||
final class PunchPointDivider: UIView {
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = UIColor(hex: 0xF0F0F0)
|
||
snp.makeConstraints { make in
|
||
make.height.equalTo(1)
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
}
|
||
|
||
private extension String {
|
||
var nonEmptyTrimmed: String? {
|
||
let text = trimmingCharacters(in: .whitespacesAndNewlines)
|
||
return text.isEmpty ? nil : text
|
||
}
|
||
}
|