// // PhotoPickerView.swift // QuickLocation // import UIKit import Photos final class PhotoPickerView: UIView { var onClose: (() -> Void)? var onToggleAlbum: (() -> Void)? var onPreview: (() -> Void)? var onConfirm: (() -> Void)? var onSelectAsset: ((Int) -> Void)? var onPickAlbum: ((Int) -> Void)? var isAlbumOpen = false { didSet { applyAlbumOpen() } } private let store: PhotoPickerStore private let maxCount: Int private var selectedIds: [String] = [] private var currentAlbumIndex = 0 private var lastGridWidth: CGFloat = 0 init(store: PhotoPickerStore, maxCount: Int) { self.store = store self.maxCount = maxCount super.init(frame: .zero) backgroundColor = UIColor(hexStr: "#2C2C2E") setupUI() applyAlbumOpen() updateBottomBar(selectedCount: 0) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() let width = collectionView.bounds.width if width > 1, abs(width - lastGridWidth) > 0.5 { lastGridWidth = width collectionView.collectionViewLayout.invalidateLayout() } } func reloadGrid() { collectionView.reloadData() } func bindAlbumStore(_ store: PhotoPickerStore) { albumListView.bindStore(store) } func reloadAlbums(currentIndex: Int) { currentAlbumIndex = currentIndex albumListView.reload(albums: store.albums, selectedIndex: currentIndex) let title = store.albums[safe: currentIndex]?.title ?? "最近项目" albumTitleLab.text = title } func applySelection(_ ids: [String]) { selectedIds = ids updateBottomBar(selectedCount: ids.count) collectionView.reloadData() } func showLimitToast() { limitBanner.isHidden = false NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideLimitToast), object: nil) perform(#selector(hideLimitToast), with: nil, afterDelay: 1.8) } func thumbnailSize() -> CGSize { let side = collectionView.bounds.width > 0 ? (collectionView.bounds.width - 3) / 4 : (UIScreen.main.bounds.width - 3) / 4 return CGSize(width: side, height: side) } // MARK: - UI private lazy var closeBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "PhotoPicker/btn_close"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) btn.addTarget(self, action: #selector(tapClose), for: .touchUpInside) return btn }() private lazy var albumTitleBtn: UIControl = { let control = UIControl() control.backgroundColor = UIColor.white.withAlphaComponent(0.12) control.cornerRadius = 16 control.addTarget(self, action: #selector(tapAlbum), for: .touchUpInside) return control }() private lazy var albumTitleLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 15, weight: .medium) label.textColor = .white label.text = "最近项目" return label }() private lazy var albumArrowView: UIImageView = { let iv = UIImageView(image: UIImage(named: "PhotoPicker/btn_album_arrow")) iv.contentMode = .scaleAspectFit return iv }() lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.minimumInteritemSpacing = 1 layout.minimumLineSpacing = 1 layout.sectionInset = .zero let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = UIColor(hexStr: "#2C2C2E") cv.register(PhotoPickerCell.self, forCellWithReuseIdentifier: "cell") cv.dataSource = self cv.delegate = self cv.alwaysBounceVertical = true return cv }() private lazy var dimView: UIControl = { let view = UIControl() view.backgroundColor = UIColor.black.withAlphaComponent(0.45) view.addTarget(self, action: #selector(tapAlbum), for: .touchUpInside) return view }() private lazy var albumListView: PhotoPickerAlbumView = { let view = PhotoPickerAlbumView() view.onSelect = { [weak self] index in self?.onPickAlbum?(index) } return view }() private lazy var bottomBar: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#1C1C1E") return view }() private lazy var previewBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("预览", for: .normal) btn.setTitleColor(UIColor.white.withAlphaComponent(0.35), for: .disabled) btn.setTitleColor(.white, for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 16) btn.contentHorizontalAlignment = .left btn.addTarget(self, action: #selector(tapPreview), for: .touchUpInside) return btn }() private lazy var confirmBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("确认", for: .normal) btn.setTitleColor(.white, for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.backgroundColor = UIColor(hexStr: "#007AFF") btn.cornerRadius = 8 btn.addTarget(self, action: #selector(tapConfirm), for: .touchUpInside) return btn }() private lazy var limitBanner: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#3A3A3C") view.isHidden = true let icon = UILabel() icon.text = "!" icon.textColor = .white icon.font = .systemFont(ofSize: 12, weight: .bold) icon.textAlignment = .center icon.backgroundColor = .clear icon.layer.borderColor = UIColor.white.cgColor icon.layer.borderWidth = 1.2 icon.layer.cornerRadius = 8 icon.clipsToBounds = true let label = UILabel() label.text = "最多选取\(maxCount)张图片哦~" label.textColor = .white label.font = .systemFont(ofSize: 14) view.addSubview(icon) view.addSubview(label) icon.layoutChain.left(24).centerY().width(16).height(16) label.layoutChain.leftToRightOfView(icon, offset: 8).centerY() return view }() private var albumListHeight: NSLayoutConstraint? private func setupUI() { addSubview(closeBtn) addSubview(albumTitleBtn) albumTitleBtn.addSubview(albumTitleLab) albumTitleBtn.addSubview(albumArrowView) addSubview(collectionView) addSubview(dimView) addSubview(albumListView) addSubview(limitBanner) addSubview(bottomBar) bottomBar.addSubview(previewBtn) bottomBar.addSubview(confirmBtn) closeBtn.layoutChain .left(16) .width(32) .height(32) closeBtn.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 6).isActive = true albumTitleBtn.layoutChain .centerX() .centerY(closeBtn) .height(32) albumTitleLab.layoutChain .left(12) .centerY() albumArrowView.layoutChain .leftToRightOfView(albumTitleLab, offset: 6) .right(8) .centerY() .width(20) .height(20) collectionView.layoutChain .topToBottomOfView(closeBtn, offset: 10) .left() .right() bottomBar.layoutChain .left() .right() .bottom() .topToBottomOfView(collectionView) let bottomH = 56 + kSafeBottomMargin bottomBar.layoutChain.height(bottomH) previewBtn.layoutChain .left(20) .top(16) .height(24) confirmBtn.layoutChain .right(16) .top(10) .width(88) .height(36) limitBanner.layoutChain .left() .right() .bottomToTopOfView(bottomBar) .height(44) dimView.layoutChain .topToBottomOfView(closeBtn, offset: 10) .left() .right() .bottomToTopOfView(bottomBar) albumListView.layoutChain .topToView(dimView) .left() .right() albumListHeight = albumListView.heightAnchor.constraint(equalToConstant: 0) albumListHeight?.isActive = true } private func applyAlbumOpen() { dimView.isHidden = !isAlbumOpen albumListView.isHidden = !isAlbumOpen let count = store.albums.count let rowH: CGFloat = 64 let maxH = min(CGFloat(count) * rowH, UIScreen.main.bounds.height * 0.42) albumListHeight?.constant = isAlbumOpen ? maxH : 0 UIView.animate(withDuration: 0.22) { self.albumArrowView.transform = self.isAlbumOpen ? CGAffineTransform(rotationAngle: .pi) : .identity self.layoutIfNeeded() } } private func updateBottomBar(selectedCount: Int) { previewBtn.isEnabled = selectedCount > 0 if selectedCount > 0 { previewBtn.setTitle("预览 (\(selectedCount))", for: .normal) } else { previewBtn.setTitle("预览", for: .normal) } confirmBtn.alpha = selectedCount > 0 ? 1 : 0.55 } @objc private func tapClose() { onClose?() } @objc private func tapAlbum() { onToggleAlbum?() } @objc private func tapPreview() { onPreview?() } @objc private func tapConfirm() { onConfirm?() } @objc private func hideLimitToast() { limitBanner.isHidden = true } } extension PhotoPickerView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { store.assets.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! PhotoPickerCell let asset = store.assets[indexPath.item] let index = selectedIds.firstIndex(of: asset.localIdentifier) cell.configure(asset: asset, selectedIndex: index, store: store, size: thumbnailSize()) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { onSelectAsset?(indexPath.item) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { thumbnailSize() } } private final class PhotoPickerCell: UICollectionViewCell { private let imageView = UIImageView() private let dimView = UIView() private let badge = UILabel() private var requestId: PHImageRequestID = PHInvalidImageRequestID private var assetId: String = "" override init(frame: CGRect) { super.init(frame: frame) imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true dimView.backgroundColor = UIColor.black.withAlphaComponent(0.28) dimView.isHidden = true badge.textAlignment = .center badge.font = .systemFont(ofSize: 13, weight: .semibold) badge.layer.cornerRadius = 11 badge.clipsToBounds = true contentView.addSubview(imageView) contentView.addSubview(dimView) contentView.addSubview(badge) imageView.layoutChain.edges() dimView.layoutChain.edges() badge.layoutChain.top(5).right(5).width(22).height(22) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(asset: PHAsset, selectedIndex: Int?, store: PhotoPickerStore, size: CGSize) { if requestId != PHInvalidImageRequestID { store.cancelRequest(requestId) } assetId = asset.localIdentifier imageView.image = nil let selected = selectedIndex != nil dimView.isHidden = !selected if let selectedIndex { badge.text = "\(selectedIndex + 1)" badge.textColor = .white badge.backgroundColor = UIColor(hexStr: "#007AFF") badge.layer.borderWidth = 0 } else { badge.text = "" badge.backgroundColor = UIColor.black.withAlphaComponent(0.18) badge.layer.borderWidth = 1.5 badge.layer.borderColor = UIColor.white.cgColor } requestId = store.requestThumbnail(for: asset, size: size) { [weak self] image in guard let self, self.assetId == asset.localIdentifier else { return } self.imageView.image = image } } override func prepareForReuse() { super.prepareForReuse() imageView.image = nil assetId = "" } }