585 lines
22 KiB
Swift
585 lines
22 KiB
Swift
//
|
|
// TravelAlbumCameraImportViewController.swift
|
|
// suixinkan
|
|
//
|
|
|
|
import SnapKit
|
|
import UIKit
|
|
|
|
/// 旅拍相册相机历史照片选择导入页。
|
|
final class TravelAlbumCameraImportViewController: UIViewController {
|
|
private enum Layout {
|
|
static let columns: CGFloat = 3
|
|
static let spacing: CGFloat = 2
|
|
static let inset: CGFloat = 2
|
|
static let headerHeight: CGFloat = 44
|
|
}
|
|
|
|
private let viewModel: TravelAlbumCameraImportViewModel
|
|
var onImportFinished: ((Int, Int, [String]) -> Void)?
|
|
var onImportCancelled: (() -> Void)?
|
|
|
|
private var collectionView: UICollectionView!
|
|
private var sections: [TravelAlbumCameraImportSection] = []
|
|
private var lastCollectionWidth: CGFloat = 0
|
|
|
|
private lazy var importBarButton = UIBarButtonItem(
|
|
title: "导入 (0)",
|
|
style: .done,
|
|
target: self,
|
|
action: #selector(importTapped)
|
|
)
|
|
private lazy var helpBarButton = UIBarButtonItem(
|
|
image: UIImage(systemName: "questionmark.circle"),
|
|
style: .plain,
|
|
target: self,
|
|
action: #selector(helpTapped)
|
|
)
|
|
private let activityIndicator = UIActivityIndicatorView(style: .large)
|
|
private let messageLabel = UILabel()
|
|
private let helpButton = UIButton(type: .system)
|
|
|
|
/// 初始化旅拍相册相机历史照片选择导入页。
|
|
init(viewModel: TravelAlbumCameraImportViewModel) {
|
|
self.viewModel = viewModel
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
title = "选择照片"
|
|
view.backgroundColor = .white
|
|
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
|
title: "取消",
|
|
style: .plain,
|
|
target: self,
|
|
action: #selector(cancelTapped)
|
|
)
|
|
navigationItem.rightBarButtonItem = importBarButton
|
|
importBarButton.isEnabled = false
|
|
setupUI()
|
|
setupConstraints()
|
|
bindViewModel()
|
|
viewModel.loadPhotos()
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
let width = collectionView.bounds.width
|
|
guard width > 0, width != lastCollectionWidth else { return }
|
|
lastCollectionWidth = width
|
|
collectionView.collectionViewLayout.invalidateLayout()
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
navigationController?.presentationController?.delegate = self
|
|
}
|
|
|
|
private func setupUI() {
|
|
let layout = UICollectionViewFlowLayout()
|
|
layout.minimumLineSpacing = Layout.spacing
|
|
layout.minimumInteritemSpacing = Layout.spacing
|
|
layout.sectionInset = UIEdgeInsets(
|
|
top: Layout.inset,
|
|
left: Layout.inset,
|
|
bottom: Layout.inset,
|
|
right: Layout.inset
|
|
)
|
|
layout.headerReferenceSize = CGSize(width: 0, height: Layout.headerHeight)
|
|
|
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
|
collectionView.backgroundColor = .white
|
|
collectionView.allowsMultipleSelection = true
|
|
collectionView.dataSource = self
|
|
collectionView.delegate = self
|
|
collectionView.register(
|
|
TravelAlbumCameraImportPhotoCell.self,
|
|
forCellWithReuseIdentifier: TravelAlbumCameraImportPhotoCell.reuseIdentifier
|
|
)
|
|
collectionView.register(
|
|
TravelAlbumCameraImportSectionHeaderView.self,
|
|
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
|
|
withReuseIdentifier: TravelAlbumCameraImportSectionHeaderView.reuseIdentifier
|
|
)
|
|
|
|
activityIndicator.hidesWhenStopped = true
|
|
messageLabel.numberOfLines = 0
|
|
messageLabel.textAlignment = .center
|
|
messageLabel.textColor = AppColor.textSecondary
|
|
messageLabel.font = .systemFont(ofSize: 15)
|
|
messageLabel.isHidden = true
|
|
|
|
helpButton.setTitle("如何切换到 MTP 模式", for: .normal)
|
|
helpButton.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
|
|
helpButton.backgroundColor = AppColor.primary.withAlphaComponent(0.10)
|
|
helpButton.setTitleColor(AppColor.primary, for: .normal)
|
|
helpButton.layer.cornerRadius = 9
|
|
helpButton.isHidden = true
|
|
helpButton.addTarget(self, action: #selector(helpTapped), for: .touchUpInside)
|
|
|
|
view.addSubview(collectionView)
|
|
view.addSubview(activityIndicator)
|
|
view.addSubview(messageLabel)
|
|
view.addSubview(helpButton)
|
|
}
|
|
|
|
private func setupConstraints() {
|
|
collectionView.snp.makeConstraints { make in
|
|
make.edges.equalTo(view.safeAreaLayoutGuide)
|
|
}
|
|
activityIndicator.snp.makeConstraints { make in
|
|
make.center.equalToSuperview()
|
|
}
|
|
messageLabel.snp.makeConstraints { make in
|
|
make.centerX.equalToSuperview()
|
|
make.centerY.equalToSuperview().offset(-28)
|
|
make.leading.greaterThanOrEqualToSuperview().offset(32)
|
|
make.trailing.lessThanOrEqualToSuperview().offset(-32)
|
|
}
|
|
helpButton.snp.makeConstraints { make in
|
|
make.top.equalTo(messageLabel.snp.bottom).offset(20)
|
|
make.centerX.equalToSuperview()
|
|
make.height.equalTo(38)
|
|
make.width.greaterThanOrEqualTo(160)
|
|
}
|
|
}
|
|
|
|
private func bindViewModel() {
|
|
viewModel.onStateUpdated = { [weak self] state in
|
|
self?.render(state)
|
|
}
|
|
viewModel.onSelectionUpdated = { [weak self] count in
|
|
self?.updateImportButton(count: count)
|
|
}
|
|
viewModel.onSonyMTPHelpVisibilityChanged = { [weak self] show in
|
|
self?.updateHelpVisibility(show)
|
|
}
|
|
}
|
|
|
|
private func render(_ state: TravelAlbumCameraImportState) {
|
|
switch state {
|
|
case .idle, .loading:
|
|
isModalInPresentation = false
|
|
collectionView.isHidden = true
|
|
messageLabel.isHidden = true
|
|
helpButton.isHidden = true
|
|
importBarButton.isEnabled = false
|
|
navigationItem.leftBarButtonItem?.isEnabled = true
|
|
activityIndicator.startAnimating()
|
|
|
|
case .loaded(let loadedSections):
|
|
isModalInPresentation = false
|
|
activityIndicator.stopAnimating()
|
|
collectionView.isHidden = false
|
|
messageLabel.isHidden = true
|
|
helpButton.isHidden = true
|
|
navigationItem.leftBarButtonItem?.isEnabled = true
|
|
sections = loadedSections
|
|
collectionView.reloadData()
|
|
|
|
case .importing(let current, let total):
|
|
isModalInPresentation = true
|
|
collectionView.isHidden = true
|
|
messageLabel.isHidden = false
|
|
messageLabel.text = "正在导入 \(current)/\(total)..."
|
|
helpButton.isHidden = true
|
|
importBarButton.isEnabled = false
|
|
navigationItem.leftBarButtonItem?.isEnabled = false
|
|
activityIndicator.startAnimating()
|
|
|
|
case .finished(let importedCount, let failedCount, let importedPhotoIds):
|
|
isModalInPresentation = false
|
|
activityIndicator.stopAnimating()
|
|
dismiss(animated: true) { [weak self] in
|
|
self?.onImportFinished?(importedCount, failedCount, importedPhotoIds)
|
|
}
|
|
|
|
case .failed(let message):
|
|
isModalInPresentation = false
|
|
activityIndicator.stopAnimating()
|
|
collectionView.isHidden = true
|
|
messageLabel.isHidden = false
|
|
messageLabel.text = message
|
|
importBarButton.isEnabled = false
|
|
navigationItem.leftBarButtonItem?.isEnabled = true
|
|
helpButton.isHidden = !viewModel.shouldShowSonyMTPHelp
|
|
}
|
|
}
|
|
|
|
private func updateHelpVisibility(_ show: Bool) {
|
|
navigationItem.rightBarButtonItems = show ? [importBarButton, helpBarButton] : [importBarButton]
|
|
if case .failed = viewModel.state {
|
|
helpButton.isHidden = !show
|
|
}
|
|
}
|
|
|
|
private func updateImportButton(count: Int) {
|
|
importBarButton.title = "导入 (\(count))"
|
|
importBarButton.isEnabled = count > 0
|
|
}
|
|
|
|
private func thumbnailPixelSize() -> Int {
|
|
guard let flow = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
|
|
return 300
|
|
}
|
|
return Int(itemSize(layout: flow).width * UIScreen.main.scale)
|
|
}
|
|
|
|
private func itemSize(layout: UICollectionViewFlowLayout) -> CGSize {
|
|
let totalSpacing = layout.sectionInset.left
|
|
+ layout.sectionInset.right
|
|
+ layout.minimumInteritemSpacing * (Layout.columns - 1)
|
|
let width = floor((collectionView.bounds.width - totalSpacing) / Layout.columns)
|
|
return CGSize(width: width, height: width + 28)
|
|
}
|
|
|
|
private func applyHeader(_ header: TravelAlbumCameraImportSectionHeaderView, at sectionIndex: Int) {
|
|
let section = sections[sectionIndex]
|
|
header.apply(
|
|
title: section.title,
|
|
count: section.photos.count,
|
|
importableCount: viewModel.importablePhotoCount(in: section),
|
|
importedCount: viewModel.importedPhotoCount(in: section),
|
|
hasImportablePhotos: viewModel.hasImportablePhotos(in: section),
|
|
fullySelected: viewModel.isSectionFullySelected(section),
|
|
partiallySelected: viewModel.isSectionPartiallySelected(section)
|
|
)
|
|
header.onToggle = { [weak self] in
|
|
self?.toggleSection(at: sectionIndex)
|
|
}
|
|
}
|
|
|
|
private func refreshVisibleHeader(at sectionIndex: Int) {
|
|
let indexPath = IndexPath(item: 0, section: sectionIndex)
|
|
guard let header = collectionView.supplementaryView(
|
|
forElementKind: UICollectionView.elementKindSectionHeader,
|
|
at: indexPath
|
|
) as? TravelAlbumCameraImportSectionHeaderView else {
|
|
return
|
|
}
|
|
applyHeader(header, at: sectionIndex)
|
|
}
|
|
|
|
private func reloadItemsWithoutAnimation(_ indexPaths: [IndexPath]) {
|
|
UIView.performWithoutAnimation {
|
|
collectionView.reloadItems(at: indexPaths)
|
|
collectionView.layoutIfNeeded()
|
|
}
|
|
}
|
|
|
|
private func toggleSection(at sectionIndex: Int) {
|
|
guard sections.indices.contains(sectionIndex) else { return }
|
|
viewModel.toggleSection(sections[sectionIndex])
|
|
let indexPaths = sections[sectionIndex].photos.indices.map {
|
|
IndexPath(item: $0, section: sectionIndex)
|
|
}
|
|
reloadItemsWithoutAnimation(indexPaths)
|
|
refreshVisibleHeader(at: sectionIndex)
|
|
}
|
|
|
|
@objc private func cancelTapped() {
|
|
onImportCancelled?()
|
|
dismiss(animated: true)
|
|
}
|
|
|
|
@objc private func importTapped() {
|
|
viewModel.importSelected()
|
|
}
|
|
|
|
@objc private func helpTapped() {
|
|
guard let url = URL(string: "https://sharesky.feishu.cn/wiki/CaAhwl3Gnin4Kqk0BVocK3SGnab?from=from_copylink") else { return }
|
|
UIApplication.shared.open(url)
|
|
}
|
|
}
|
|
|
|
extension TravelAlbumCameraImportViewController: UICollectionViewDataSource {
|
|
func numberOfSections(in collectionView: UICollectionView) -> Int {
|
|
sections.count
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
sections[section].photos.count
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
let cell = collectionView.dequeueReusableCell(
|
|
withReuseIdentifier: TravelAlbumCameraImportPhotoCell.reuseIdentifier,
|
|
for: indexPath
|
|
) as! TravelAlbumCameraImportPhotoCell
|
|
let photo = sections[indexPath.section].photos[indexPath.item]
|
|
cell.apply(
|
|
photo: photo,
|
|
selected: viewModel.isPhotoSelected(id: photo.id),
|
|
alreadyImported: viewModel.isPhotoAlreadyInAlbum(id: photo.id),
|
|
maxPixelSize: thumbnailPixelSize()
|
|
) { [weak self] photo, maxPixelSize in
|
|
await self?.viewModel.thumbnailData(for: photo, maxPixelSize: maxPixelSize)
|
|
}
|
|
return cell
|
|
}
|
|
|
|
func collectionView(
|
|
_ collectionView: UICollectionView,
|
|
viewForSupplementaryElementOfKind kind: String,
|
|
at indexPath: IndexPath
|
|
) -> UICollectionReusableView {
|
|
guard kind == UICollectionView.elementKindSectionHeader else {
|
|
return UICollectionReusableView()
|
|
}
|
|
let header = collectionView.dequeueReusableSupplementaryView(
|
|
ofKind: kind,
|
|
withReuseIdentifier: TravelAlbumCameraImportSectionHeaderView.reuseIdentifier,
|
|
for: indexPath
|
|
) as! TravelAlbumCameraImportSectionHeaderView
|
|
applyHeader(header, at: indexPath.section)
|
|
return header
|
|
}
|
|
}
|
|
|
|
extension TravelAlbumCameraImportViewController: UICollectionViewDelegateFlowLayout {
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
let photo = sections[indexPath.section].photos[indexPath.item]
|
|
guard !viewModel.isPhotoAlreadyInAlbum(id: photo.id) else { return }
|
|
viewModel.togglePhoto(id: photo.id)
|
|
reloadItemsWithoutAnimation([indexPath])
|
|
refreshVisibleHeader(at: indexPath.section)
|
|
}
|
|
|
|
func collectionView(
|
|
_ collectionView: UICollectionView,
|
|
layout collectionViewLayout: UICollectionViewLayout,
|
|
sizeForItemAt indexPath: IndexPath
|
|
) -> CGSize {
|
|
guard let flow = collectionViewLayout as? UICollectionViewFlowLayout else {
|
|
return CGSize(width: 120, height: 148)
|
|
}
|
|
return itemSize(layout: flow)
|
|
}
|
|
}
|
|
|
|
extension TravelAlbumCameraImportViewController: UIAdaptivePresentationControllerDelegate {
|
|
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
|
onImportCancelled?()
|
|
}
|
|
}
|
|
|
|
/// 相机历史照片导入 Cell。
|
|
private final class TravelAlbumCameraImportPhotoCell: UICollectionViewCell {
|
|
static let reuseIdentifier = "TravelAlbumCameraImportPhotoCell"
|
|
|
|
private let imageView = UIImageView()
|
|
private let filenameLabel = UILabel()
|
|
private let checkmarkView = UIImageView()
|
|
private let selectedOverlayView = UIView()
|
|
private let importedOverlayView = UIView()
|
|
private let importedBadgeLabel = UILabel()
|
|
private var representedPhotoId: String?
|
|
private var loadToken = UUID()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
setupUI()
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func prepareForReuse() {
|
|
super.prepareForReuse()
|
|
representedPhotoId = nil
|
|
loadToken = UUID()
|
|
imageView.image = nil
|
|
filenameLabel.text = nil
|
|
applySelection(false, alreadyImported: false)
|
|
applyImported(false)
|
|
}
|
|
|
|
/// 绑定相机照片与选择状态。
|
|
func apply(
|
|
photo: CameraObject,
|
|
selected: Bool,
|
|
alreadyImported: Bool,
|
|
maxPixelSize: Int,
|
|
thumbnailProvider: @escaping (CameraObject, Int) async -> Data?
|
|
) {
|
|
representedPhotoId = photo.id
|
|
filenameLabel.text = photo.filename
|
|
setPlaceholderImage()
|
|
applyImported(alreadyImported)
|
|
applySelection(selected, alreadyImported: alreadyImported)
|
|
|
|
let token = UUID()
|
|
loadToken = token
|
|
Task { @MainActor in
|
|
guard let data = await thumbnailProvider(photo, maxPixelSize),
|
|
self.loadToken == token,
|
|
self.representedPhotoId == photo.id,
|
|
let image = UIImage(data: data) else {
|
|
return
|
|
}
|
|
self.imageView.image = image
|
|
self.imageView.tintColor = nil
|
|
}
|
|
}
|
|
|
|
private func setupUI() {
|
|
contentView.backgroundColor = AppColor.pageBackground
|
|
contentView.clipsToBounds = true
|
|
contentView.layer.cornerRadius = 4
|
|
|
|
imageView.contentMode = .scaleAspectFill
|
|
imageView.clipsToBounds = true
|
|
selectedOverlayView.backgroundColor = AppColor.primary.withAlphaComponent(0.25)
|
|
selectedOverlayView.isHidden = true
|
|
importedOverlayView.backgroundColor = UIColor.black.withAlphaComponent(0.35)
|
|
importedOverlayView.isHidden = true
|
|
|
|
importedBadgeLabel.text = "已在相册"
|
|
importedBadgeLabel.font = .systemFont(ofSize: 9, weight: .semibold)
|
|
importedBadgeLabel.textColor = .white
|
|
importedBadgeLabel.textAlignment = .center
|
|
importedBadgeLabel.backgroundColor = UIColor.black.withAlphaComponent(0.55)
|
|
importedBadgeLabel.layer.cornerRadius = 4
|
|
importedBadgeLabel.clipsToBounds = true
|
|
importedBadgeLabel.isHidden = true
|
|
|
|
checkmarkView.image = UIImage(systemName: "checkmark.circle.fill")
|
|
checkmarkView.tintColor = AppColor.primary
|
|
checkmarkView.isHidden = true
|
|
|
|
filenameLabel.font = .systemFont(ofSize: 9, weight: .medium)
|
|
filenameLabel.textColor = AppColor.textSecondary
|
|
filenameLabel.textAlignment = .center
|
|
filenameLabel.numberOfLines = 2
|
|
|
|
[imageView, selectedOverlayView, importedOverlayView, importedBadgeLabel, checkmarkView, filenameLabel].forEach {
|
|
contentView.addSubview($0)
|
|
}
|
|
|
|
imageView.snp.makeConstraints { make in
|
|
make.top.leading.trailing.equalToSuperview()
|
|
make.height.equalTo(contentView.snp.width)
|
|
}
|
|
selectedOverlayView.snp.makeConstraints { make in
|
|
make.top.leading.trailing.equalToSuperview()
|
|
make.bottom.equalTo(imageView)
|
|
}
|
|
importedOverlayView.snp.makeConstraints { make in
|
|
make.top.leading.trailing.equalToSuperview()
|
|
make.bottom.equalTo(imageView)
|
|
}
|
|
importedBadgeLabel.snp.makeConstraints { make in
|
|
make.center.equalTo(imageView)
|
|
make.height.equalTo(18)
|
|
make.width.greaterThanOrEqualTo(52)
|
|
}
|
|
checkmarkView.snp.makeConstraints { make in
|
|
make.top.trailing.equalToSuperview().inset(4)
|
|
make.size.equalTo(22)
|
|
}
|
|
filenameLabel.snp.makeConstraints { make in
|
|
make.top.equalTo(imageView.snp.bottom).offset(2)
|
|
make.leading.trailing.equalToSuperview().inset(2)
|
|
make.bottom.lessThanOrEqualToSuperview().offset(-2)
|
|
}
|
|
}
|
|
|
|
private func setPlaceholderImage() {
|
|
let config = UIImage.SymbolConfiguration(pointSize: 28, weight: .light)
|
|
imageView.image = UIImage(systemName: "photo", withConfiguration: config)
|
|
imageView.tintColor = AppColor.textTertiary
|
|
}
|
|
|
|
private func applySelection(_ selected: Bool, alreadyImported: Bool) {
|
|
let showSelection = selected && !alreadyImported
|
|
selectedOverlayView.isHidden = !showSelection
|
|
checkmarkView.isHidden = !showSelection
|
|
contentView.layer.borderWidth = showSelection ? 2 : 0
|
|
contentView.layer.borderColor = showSelection ? AppColor.primary.cgColor : nil
|
|
}
|
|
|
|
private func applyImported(_ alreadyImported: Bool) {
|
|
importedOverlayView.isHidden = !alreadyImported
|
|
importedBadgeLabel.isHidden = !alreadyImported
|
|
contentView.alpha = alreadyImported ? 0.55 : 1
|
|
}
|
|
}
|
|
|
|
/// 相机历史照片导入分组 Header。
|
|
private final class TravelAlbumCameraImportSectionHeaderView: UICollectionReusableView {
|
|
static let reuseIdentifier = "TravelAlbumCameraImportSectionHeaderView"
|
|
|
|
var onToggle: (() -> Void)?
|
|
|
|
private let checkButton = UIButton(type: .system)
|
|
private let titleLabel = UILabel()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
setupUI()
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
/// 绑定分组信息。
|
|
func apply(
|
|
title: String,
|
|
count: Int,
|
|
importableCount: Int,
|
|
importedCount: Int,
|
|
hasImportablePhotos: Bool,
|
|
fullySelected: Bool,
|
|
partiallySelected: Bool
|
|
) {
|
|
titleLabel.text = importedCount > 0 ? "\(title) · 可导入 \(importableCount) / 共 \(count) 张" : "\(title) · \(count) 张"
|
|
checkButton.isEnabled = hasImportablePhotos
|
|
let symbolName: String
|
|
if !hasImportablePhotos {
|
|
symbolName = "circle"
|
|
} else if fullySelected {
|
|
symbolName = "checkmark.circle.fill"
|
|
} else if partiallySelected {
|
|
symbolName = "minus.circle.fill"
|
|
} else {
|
|
symbolName = "circle"
|
|
}
|
|
checkButton.setImage(UIImage(systemName: symbolName), for: .normal)
|
|
checkButton.tintColor = hasImportablePhotos && (fullySelected || partiallySelected) ? AppColor.primary : AppColor.textTertiary
|
|
}
|
|
|
|
private func setupUI() {
|
|
backgroundColor = .white
|
|
checkButton.addTarget(self, action: #selector(toggleTapped), for: .touchUpInside)
|
|
titleLabel.font = .systemFont(ofSize: 15, weight: .semibold)
|
|
titleLabel.textColor = AppColor.textPrimary
|
|
|
|
addSubview(checkButton)
|
|
addSubview(titleLabel)
|
|
checkButton.snp.makeConstraints { make in
|
|
make.leading.equalToSuperview().offset(12)
|
|
make.centerY.equalToSuperview()
|
|
make.size.equalTo(28)
|
|
}
|
|
titleLabel.snp.makeConstraints { make in
|
|
make.leading.equalTo(checkButton.snp.trailing).offset(8)
|
|
make.trailing.equalToSuperview().offset(-12)
|
|
make.centerY.equalToSuperview()
|
|
}
|
|
}
|
|
|
|
@objc private func toggleTapped() {
|
|
onToggle?()
|
|
}
|
|
}
|