524 lines
20 KiB
Swift
524 lines
20 KiB
Swift
//
|
|
// ScheduleDetailView.swift
|
|
// QuickLocation
|
|
//
|
|
// Created by 八条 on 2026/6/25.
|
|
//
|
|
|
|
import UIKit
|
|
import RxSwift
|
|
import RxCocoa
|
|
import AMapNaviKit
|
|
|
|
final class ScheduleDetailView: UIView {
|
|
|
|
private let disposeBag = DisposeBag()
|
|
private let panelTopOffset = max(kNaviHeight + 190, UIScreen.main.bounds.height * 0.44)
|
|
|
|
var mapBottomPadding: CGFloat {
|
|
max(220, bounds.height - detailPanelView.frame.minY + 24)
|
|
}
|
|
|
|
func configure(_ model: ScheduleModel) {
|
|
dateLab.text = formattedDate(model.timestamp)
|
|
editBtn.isHidden = !model.is_own
|
|
}
|
|
|
|
func cleanupMap() {
|
|
mapView?.delegate = nil
|
|
mapView?.removeFromSuperview()
|
|
mapView = nil
|
|
}
|
|
|
|
private func setupUI() {
|
|
backgroundColor = .white
|
|
addSubview(mapView)
|
|
addSubview(detailPanelView)
|
|
addSubview(backBtn)
|
|
addSubview(editBtn)
|
|
|
|
detailPanelView.addSubview(titleIconView)
|
|
detailPanelView.addSubview(titleLab)
|
|
detailPanelView.addSubview(dateLab)
|
|
detailPanelView.addSubview(tableView)
|
|
|
|
[mapView, detailPanelView, backBtn, editBtn, titleIconView, titleLab, dateLab, tableView]
|
|
.forEach { $0?.translatesAutoresizingMaskIntoConstraints = false }
|
|
|
|
NSLayoutConstraint.activate([
|
|
mapView.topAnchor.constraint(equalTo: topAnchor),
|
|
mapView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
mapView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
mapView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
|
|
backBtn.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10),
|
|
backBtn.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18),
|
|
backBtn.widthAnchor.constraint(equalToConstant: 44),
|
|
backBtn.heightAnchor.constraint(equalToConstant: 44),
|
|
|
|
editBtn.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10),
|
|
editBtn.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -18),
|
|
editBtn.widthAnchor.constraint(equalToConstant: 44),
|
|
editBtn.heightAnchor.constraint(equalToConstant: 44),
|
|
|
|
detailPanelView.topAnchor.constraint(equalTo: topAnchor, constant: panelTopOffset),
|
|
detailPanelView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
detailPanelView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
detailPanelView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
|
|
titleIconView.leadingAnchor.constraint(equalTo: detailPanelView.leadingAnchor, constant: 18),
|
|
titleIconView.topAnchor.constraint(equalTo: detailPanelView.topAnchor, constant: 23),
|
|
titleIconView.widthAnchor.constraint(equalToConstant: 18),
|
|
titleIconView.heightAnchor.constraint(equalToConstant: 18),
|
|
|
|
titleLab.leadingAnchor.constraint(equalTo: titleIconView.trailingAnchor, constant: 8),
|
|
titleLab.centerYAnchor.constraint(equalTo: titleIconView.centerYAnchor),
|
|
|
|
dateLab.trailingAnchor.constraint(equalTo: detailPanelView.trailingAnchor, constant: -20),
|
|
dateLab.centerYAnchor.constraint(equalTo: titleLab.centerYAnchor),
|
|
dateLab.leadingAnchor.constraint(greaterThanOrEqualTo: titleLab.trailingAnchor, constant: 12),
|
|
|
|
tableView.topAnchor.constraint(equalTo: titleIconView.bottomAnchor, constant: 14),
|
|
tableView.leadingAnchor.constraint(equalTo: detailPanelView.leadingAnchor),
|
|
tableView.trailingAnchor.constraint(equalTo: detailPanelView.trailingAnchor),
|
|
tableView.bottomAnchor.constraint(equalTo: detailPanelView.bottomAnchor)
|
|
])
|
|
}
|
|
|
|
private func setupRx() {
|
|
backBtn.rx.tap
|
|
.subscribe(onNext: { AppRouter.shared.popOrDismiss() })
|
|
.disposed(by: disposeBag)
|
|
}
|
|
|
|
private func formattedDate(_ timestamp: Int64) -> String {
|
|
guard timestamp > 0 else { return "" }
|
|
let formatter = DateFormatter()
|
|
formatter.locale = Locale(identifier: "zh_CN")
|
|
formatter.dateFormat = "yyyy年M月d日"
|
|
return formatter.string(from: Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000))
|
|
}
|
|
|
|
lazy var mapView: MAMapView! = {
|
|
let view = MAMapView()
|
|
view.zoomLevel = 14
|
|
view.showsUserLocation = false
|
|
view.showsCompass = false
|
|
view.userTrackingMode = .none
|
|
return view
|
|
}()
|
|
|
|
lazy var detailPanelView: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = UIColor(hexStr: "#FAFAFA")
|
|
view.layer.cornerRadius = 24
|
|
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
view.clipsToBounds = true
|
|
return view
|
|
}()
|
|
|
|
lazy var backBtn: UIButton = {
|
|
let button = UIButton(type: .custom)
|
|
button.setImage(UIImage(named: "Common/back"), for: .normal)
|
|
button.backgroundColor = .clear
|
|
button.cornerRadius = 12
|
|
button.layer.shadowColor = UIColor.black.withAlphaComponent(0.12).cgColor
|
|
button.layer.shadowOpacity = 1
|
|
button.layer.shadowRadius = 8
|
|
button.layer.shadowOffset = CGSize(width: 0, height: 3)
|
|
button.clipsToBounds = false
|
|
return button
|
|
}()
|
|
|
|
lazy var editBtn: UIButton = {
|
|
let button = UIButton(type: .custom)
|
|
button.setImage(UIImage(named: "IM/setting"), for: .normal)
|
|
button.backgroundColor = UIColor.white.withAlphaComponent(0.92)
|
|
button.cornerRadius = 12
|
|
button.layer.shadowColor = UIColor.black.withAlphaComponent(0.12).cgColor
|
|
button.layer.shadowOpacity = 1
|
|
button.layer.shadowRadius = 8
|
|
button.layer.shadowOffset = CGSize(width: 0, height: 3)
|
|
button.clipsToBounds = false
|
|
button.isHidden = true
|
|
return button
|
|
}()
|
|
|
|
private lazy var titleIconView: UIImageView = {
|
|
let view = UIImageView(image: UIImage(named: "Schedule/title_location"))
|
|
return view
|
|
}()
|
|
|
|
private lazy var titleLab: UILabel = {
|
|
let label = UILabel()
|
|
label.text = "行程详情"
|
|
label.font = .systemFont(ofSize: 18, weight: .bold)
|
|
label.textColor = UIColor(hexStr: "#293445")
|
|
return label
|
|
}()
|
|
|
|
private lazy var dateLab: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
|
label.textColor = UIColor(hexStr: "#293445")
|
|
return label
|
|
}()
|
|
|
|
lazy var tableView: UITableView = {
|
|
let view = UITableView(frame: .zero, style: .plain)
|
|
view.backgroundColor = .clear
|
|
view.separatorStyle = .none
|
|
view.showsVerticalScrollIndicator = false
|
|
view.estimatedRowHeight = 150
|
|
view.rowHeight = UITableView.automaticDimension
|
|
view.contentInset = UIEdgeInsets(top: 4, left: 0, bottom: kSafeBottomMargin + 16, right: 0)
|
|
view.register(ScheduleTimelineCell.self)
|
|
return view
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
setupUI()
|
|
setupRx()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
final class ScheduleTimelineCell: UITableViewCell {
|
|
|
|
func configure(_ item: ScheduleTimelineItem) {
|
|
markerView.role = item.role
|
|
upperLine.isHidden = item.isFirst
|
|
lowerLine.isHidden = item.isLast
|
|
timeLab.text = Self.timeText(item.point.expected_timestamp)
|
|
locationLab.text = Self.locationText(item.point)
|
|
let remark = item.point.remark.isEmpty ? "无备注" : item.point.remark
|
|
remarkLab.text = "备注:\(remark)"
|
|
configureEvents(item.point.events)
|
|
|
|
if let duration = item.durationToNext {
|
|
durationBadge.configure(
|
|
title: (item.role == .waypoint ? "停留 " : "") + Self.durationText(duration)
|
|
)
|
|
durationRow.isHidden = false
|
|
} else {
|
|
durationRow.isHidden = true
|
|
}
|
|
}
|
|
|
|
private func setupUI() {
|
|
selectionStyle = .none
|
|
backgroundColor = .clear
|
|
contentView.backgroundColor = .clear
|
|
|
|
contentView.addSubview(upperLine)
|
|
contentView.addSubview(lowerLine)
|
|
contentView.addSubview(markerView)
|
|
contentView.addSubview(bodyStack)
|
|
bodyStack.addArrangedSubview(headerRow)
|
|
bodyStack.addArrangedSubview(remarkLab)
|
|
bodyStack.addArrangedSubview(eventScrollView)
|
|
bodyStack.addArrangedSubview(durationRow)
|
|
eventScrollView.addSubview(eventStackView)
|
|
durationRow.addSubview(durationBadge)
|
|
headerRow.addSubview(timeLab)
|
|
headerRow.addSubview(locationLab)
|
|
|
|
[upperLine, lowerLine, markerView, bodyStack, headerRow, timeLab, locationLab,
|
|
eventScrollView, eventStackView, durationRow, durationBadge]
|
|
.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
|
|
|
|
let eventHeight = eventScrollView.heightAnchor.constraint(equalToConstant: 42)
|
|
eventHeight.priority = .defaultHigh
|
|
let durationHeight = durationRow.heightAnchor.constraint(equalToConstant: 40)
|
|
durationHeight.priority = .defaultHigh
|
|
|
|
NSLayoutConstraint.activate([
|
|
markerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
|
|
markerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
|
|
markerView.widthAnchor.constraint(equalToConstant: 22),
|
|
markerView.heightAnchor.constraint(equalToConstant: 22),
|
|
|
|
upperLine.centerXAnchor.constraint(equalTo: markerView.centerXAnchor),
|
|
upperLine.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
upperLine.bottomAnchor.constraint(equalTo: markerView.centerYAnchor),
|
|
upperLine.widthAnchor.constraint(equalToConstant: 1),
|
|
|
|
lowerLine.centerXAnchor.constraint(equalTo: markerView.centerXAnchor),
|
|
lowerLine.topAnchor.constraint(equalTo: markerView.centerYAnchor),
|
|
lowerLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
|
lowerLine.widthAnchor.constraint(equalToConstant: 1),
|
|
|
|
bodyStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 62),
|
|
bodyStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
|
bodyStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 14),
|
|
bodyStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16),
|
|
|
|
headerRow.heightAnchor.constraint(greaterThanOrEqualToConstant: 26),
|
|
timeLab.leadingAnchor.constraint(equalTo: headerRow.leadingAnchor),
|
|
timeLab.centerYAnchor.constraint(equalTo: headerRow.centerYAnchor),
|
|
timeLab.widthAnchor.constraint(equalToConstant: 64),
|
|
|
|
locationLab.leadingAnchor.constraint(equalTo: timeLab.trailingAnchor, constant: 8),
|
|
locationLab.trailingAnchor.constraint(equalTo: headerRow.trailingAnchor),
|
|
locationLab.centerYAnchor.constraint(equalTo: headerRow.centerYAnchor),
|
|
|
|
eventHeight,
|
|
eventStackView.leadingAnchor.constraint(equalTo: eventScrollView.contentLayoutGuide.leadingAnchor),
|
|
eventStackView.trailingAnchor.constraint(equalTo: eventScrollView.contentLayoutGuide.trailingAnchor),
|
|
eventStackView.topAnchor.constraint(equalTo: eventScrollView.contentLayoutGuide.topAnchor),
|
|
eventStackView.bottomAnchor.constraint(equalTo: eventScrollView.contentLayoutGuide.bottomAnchor),
|
|
eventStackView.heightAnchor.constraint(equalTo: eventScrollView.frameLayoutGuide.heightAnchor),
|
|
|
|
durationHeight,
|
|
durationBadge.leadingAnchor.constraint(equalTo: durationRow.leadingAnchor),
|
|
durationBadge.centerYAnchor.constraint(equalTo: durationRow.centerYAnchor),
|
|
durationBadge.trailingAnchor.constraint(lessThanOrEqualTo: durationRow.trailingAnchor)
|
|
])
|
|
}
|
|
|
|
private func configureEvents(_ events: [SchedulePointEventModel]) {
|
|
eventStackView.arrangedSubviews.forEach {
|
|
eventStackView.removeArrangedSubview($0)
|
|
$0.removeFromSuperview()
|
|
}
|
|
eventScrollView.isHidden = events.isEmpty
|
|
for event in events {
|
|
let chip = ScheduleTimelineEventChip()
|
|
chip.configure(time: Self.timeText(event.arrival_timestamp), text: event.text)
|
|
eventStackView.addArrangedSubview(chip)
|
|
}
|
|
}
|
|
|
|
private static func locationText(_ point: SchedulePointModel) -> String {
|
|
if !point.street.isEmpty { return point.street }
|
|
if !point.formatted_address.isEmpty { return point.formatted_address }
|
|
return "暂无地点"
|
|
}
|
|
|
|
private static func timeText(_ timestamp: Int64) -> String {
|
|
guard timestamp > 0 else { return "--:--" }
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "HH:mm"
|
|
return formatter.string(from: Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000))
|
|
}
|
|
|
|
private static func durationText(_ milliseconds: Int64) -> String {
|
|
let minutes = max(1, milliseconds / 60_000)
|
|
let hours = minutes / 60
|
|
let remainder = minutes % 60
|
|
return hours > 0 ? "\(hours)小时\(remainder)分" : "\(minutes)分钟"
|
|
}
|
|
|
|
private lazy var upperLine = ScheduleTimelineDashLineView()
|
|
private lazy var lowerLine = ScheduleTimelineDashLineView()
|
|
private lazy var markerView = ScheduleTimelineMarkerView()
|
|
|
|
private lazy var bodyStack: UIStackView = {
|
|
let view = UIStackView()
|
|
view.axis = .vertical
|
|
view.alignment = .fill
|
|
view.spacing = 10
|
|
return view
|
|
}()
|
|
|
|
private lazy var headerRow = UIView()
|
|
|
|
private lazy var timeLab: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 16, weight: .bold)
|
|
label.textColor = UIColor(hexStr: "#293445")
|
|
return label
|
|
}()
|
|
|
|
private lazy var locationLab: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 16, weight: .bold)
|
|
label.textColor = UIColor(hexStr: "#293445")
|
|
label.numberOfLines = 1
|
|
label.lineBreakMode = .byTruncatingTail
|
|
return label
|
|
}()
|
|
|
|
private lazy var remarkLab: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 13, weight: .regular)
|
|
label.textColor = UIColor(hexStr: "#8B8B8B")
|
|
label.numberOfLines = 0
|
|
return label
|
|
}()
|
|
|
|
private lazy var eventScrollView: UIScrollView = {
|
|
let view = UIScrollView()
|
|
view.showsHorizontalScrollIndicator = false
|
|
view.alwaysBounceHorizontal = false
|
|
return view
|
|
}()
|
|
|
|
private lazy var eventStackView: UIStackView = {
|
|
let view = UIStackView()
|
|
view.axis = .horizontal
|
|
view.alignment = .center
|
|
view.spacing = 8
|
|
return view
|
|
}()
|
|
|
|
private lazy var durationRow = UIView()
|
|
private lazy var durationBadge = ScheduleTimelineDurationBadge()
|
|
|
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
setupUI()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
private final class ScheduleTimelineMarkerView: UIView {
|
|
var role: ScheduleTimelineRole = .waypoint {
|
|
didSet { setNeedsDisplay() }
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = .clear
|
|
isOpaque = false
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func draw(_ rect: CGRect) {
|
|
guard let context = UIGraphicsGetCurrentContext() else { return }
|
|
context.clear(rect)
|
|
switch role {
|
|
case .waypoint:
|
|
let dotRect = CGRect(x: rect.midX - 5, y: rect.midY - 5, width: 10, height: 10)
|
|
context.setFillColor(UIColor(hexStr: "#293445").cgColor)
|
|
context.fillEllipse(in: dotRect)
|
|
let innerDotRect = CGRect(x: rect.midX - 2, y: rect.midY - 2, width: 4, height: 4)
|
|
context.setFillColor(UIColor.white.cgColor)
|
|
context.fillEllipse(in: innerDotRect)
|
|
case .start, .end:
|
|
let color = role == .start ? UIColor(hexStr: "#35C8F4") : UIColor(hexStr: "#FF5665")
|
|
context.setFillColor(color.cgColor)
|
|
context.fillEllipse(in: rect.insetBy(dx: 1, dy: 1))
|
|
let text = role == .start ? "始" : "终"
|
|
let attributes: [NSAttributedString.Key: Any] = [
|
|
.font: UIFont.systemFont(ofSize: 11, weight: .bold),
|
|
.foregroundColor: UIColor.white
|
|
]
|
|
let size = text.size(withAttributes: attributes)
|
|
text.draw(at: CGPoint(x: rect.midX - size.width / 2, y: rect.midY - size.height / 2),
|
|
withAttributes: attributes)
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class ScheduleTimelineDashLineView: UIView {
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
layer.sublayers?.forEach { if $0 is CAShapeLayer { $0.removeFromSuperlayer() } }
|
|
let line = CAShapeLayer()
|
|
line.strokeColor = UIColor(hexStr: "#D8D8D8").cgColor
|
|
line.lineWidth = 1.5
|
|
line.lineDashPattern = [5, 5]
|
|
let path = UIBezierPath()
|
|
path.move(to: CGPoint(x: bounds.midX, y: 0))
|
|
path.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY))
|
|
line.path = path.cgPath
|
|
layer.addSublayer(line)
|
|
}
|
|
}
|
|
|
|
private final class ScheduleTimelineEventChip: UIView {
|
|
func configure(time: String, text: String) {
|
|
let status = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
label.text = "\(time) \(status.isEmpty ? "已到达" : status)"
|
|
}
|
|
|
|
private let label: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
label.textColor = UIColor(hexStr: "#293445")
|
|
return label
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = .white
|
|
cornerRadius = 10
|
|
layer.shadowColor = UIColor.black.withAlphaComponent(0.06).cgColor
|
|
layer.shadowOpacity = 1
|
|
layer.shadowRadius = 8
|
|
layer.shadowOffset = CGSize(width: 0, height: 2)
|
|
clipsToBounds = false
|
|
addSubview(label)
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 13),
|
|
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -13),
|
|
label.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
heightAnchor.constraint(equalToConstant: 38)
|
|
])
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
private final class ScheduleTimelineDurationBadge: UIView {
|
|
func configure(title: String) {
|
|
titleLab.text = title
|
|
}
|
|
|
|
private let iconView: UIImageView = {
|
|
let view = UIImageView(image: UIImage(named: "Schedule/clock"))
|
|
view.contentMode = .scaleAspectFit
|
|
return view
|
|
}()
|
|
|
|
private let titleLab: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
label.textColor = UIColor(hexStr: "#293445")
|
|
return label
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = .white
|
|
cornerRadius = 10
|
|
layer.shadowColor = UIColor.black.withAlphaComponent(0.05).cgColor
|
|
layer.shadowOpacity = 1
|
|
layer.shadowRadius = 8
|
|
layer.shadowOffset = CGSize(width: 0, height: 2)
|
|
clipsToBounds = false
|
|
addSubview(iconView)
|
|
addSubview(titleLab)
|
|
iconView.translatesAutoresizingMaskIntoConstraints = false
|
|
titleLab.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
|
|
iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
iconView.widthAnchor.constraint(equalToConstant: 18),
|
|
iconView.heightAnchor.constraint(equalToConstant: 18),
|
|
titleLab.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 7),
|
|
titleLab.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
|
|
titleLab.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
heightAnchor.constraint(equalToConstant: 38)
|
|
])
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|