Files
suixinkan_uikit/suixinkan/UI/TravelAlbum/TravelAlbumPhotoPreviewViewController.swift
2026-07-31 16:48:28 +08:00

1352 lines
58 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// TravelAlbumPhotoPreviewViewController.swift
// suixinkan
//
import CoreImage
import Kingfisher
import SnapKit
import UIKit
/// AI
final class TravelAlbumPhotoPreviewViewController: UIViewController {
///
var onDeleted: (() -> Void)?
private var previewState: TravelAlbumPhotoPreviewState
private let api: any TravelAlbumServing
private let imageView = UIImageView()
private let bottomBar = UIView()
private let bottomContentStack = UIStackView()
private let comparisonBar = UIStackView()
private let versionControl = UISegmentedControl(items: ["原图", "精修后"])
private var visibleDisplayModes: [TravelAlbumAIEditResultState.DisplayMode] = [.original, .edited]
private let highResolutionButton = UIButton(type: .system)
private let actionStack = UIStackView()
private let aiEditButton = TravelAlbumPreviewActionButton(
title: "AI修图",
icon: UIImage(named: "travel_album_preview_ai_edit") ?? UIImage(systemName: "wand.and.stars")
)
private let aiEditResultStore = TravelAlbumAIEditResultStore.shared
private let previewHeaderView = TravelAlbumPreviewHeaderView()
private var originalImage: UIImage?
private var originalImagesByMaterialID: [Int: UIImage] = [:]
init(materials: [TravelAlbumMaterial], initialIndex: Int, api: any TravelAlbumServing) {
precondition(!materials.isEmpty, "照片预览至少需要一张素材")
previewState = TravelAlbumPhotoPreviewState(materials: materials, initialIndex: initialIndex)
self.api = api
super.init(nibName: nil, bundle: nil)
//
modalPresentationStyle = .fullScreen
modalTransitionStyle = .crossDissolve
}
///
convenience init(material: TravelAlbumMaterial, api: any TravelAlbumServing) {
self.init(materials: [material], initialIndex: 0, api: api)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
setupNavigationBar()
setupUI()
NotificationCenter.default.addObserver(
self,
selector: #selector(aiEditResultDidChange(_:)),
name: TravelAlbumAIEditResultStore.didChangeNotification,
object: aiEditResultStore
)
updateAIEditControls()
loadImage()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
.lightContent
}
/// 使
private func setupNavigationBar() {
previewHeaderView.onBack = { [weak self] in
self?.backTapped()
}
view.addSubview(previewHeaderView)
previewHeaderView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide)
make.leading.trailing.equalToSuperview()
make.height.equalTo(68)
}
updateNavigationTitle()
}
private func setupUI() {
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.image = UIImage(systemName: "photo")
imageView.tintColor = UIColor.white.withAlphaComponent(0.5)
imageView.isUserInteractionEnabled = true
let previousGesture = UISwipeGestureRecognizer(target: self, action: #selector(photoSwiped(_:)))
previousGesture.direction = .right
let nextGesture = UISwipeGestureRecognizer(target: self, action: #selector(photoSwiped(_:)))
nextGesture.direction = .left
imageView.addGestureRecognizer(previousGesture)
imageView.addGestureRecognizer(nextGesture)
bottomBar.backgroundColor = .black
bottomBar.layer.borderColor = UIColor.white.withAlphaComponent(0.12).cgColor
bottomBar.layer.borderWidth = 1 / UIScreen.main.scale
bottomContentStack.axis = .vertical
bottomContentStack.spacing = 10
comparisonBar.axis = .horizontal
comparisonBar.alignment = .center
comparisonBar.distribution = .equalSpacing
comparisonBar.isHidden = true
versionControl.selectedSegmentIndex = 1
versionControl.selectedSegmentTintColor = UIColor.white.withAlphaComponent(0.2)
versionControl.setTitleTextAttributes([.foregroundColor: UIColor.white.withAlphaComponent(0.7)], for: .normal)
versionControl.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
versionControl.addTarget(self, action: #selector(versionChanged), for: .valueChanged)
highResolutionButton.setTitle("查看高清图", for: .normal)
highResolutionButton.setTitleColor(.white, for: .normal)
highResolutionButton.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium)
highResolutionButton.backgroundColor = UIColor.white.withAlphaComponent(0.18)
highResolutionButton.layer.cornerRadius = 16
highResolutionButton.addTarget(self, action: #selector(highResolutionTapped), for: .touchUpInside)
actionStack.axis = .horizontal
actionStack.distribution = .fillEqually
aiEditButton.addTarget(self, action: #selector(aiEditTapped), for: .touchUpInside)
actionStack.addArrangedSubview(aiEditButton)
[
("删除", "travel_album_preview_delete", "trash", #selector(deleteTapped)),
("刷新", "travel_album_preview_refresh", "arrow.clockwise", #selector(refreshTapped)),
].forEach { title, assetName, fallbackIcon, action in
let button = TravelAlbumPreviewActionButton(
title: title,
icon: UIImage(named: assetName) ?? UIImage(systemName: fallbackIcon)
)
button.addTarget(self, action: action, for: .touchUpInside)
actionStack.addArrangedSubview(button)
}
view.addSubview(imageView)
view.addSubview(bottomBar)
bottomBar.addSubview(bottomContentStack)
bottomContentStack.addArrangedSubview(comparisonBar)
bottomContentStack.addArrangedSubview(actionStack)
comparisonBar.addArrangedSubview(versionControl)
comparisonBar.addArrangedSubview(highResolutionButton)
bottomBar.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
}
bottomContentStack.snp.makeConstraints { make in
make.top.equalToSuperview().offset(12)
make.leading.trailing.equalToSuperview().inset(20)
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-10)
}
comparisonBar.snp.makeConstraints { make in
make.height.equalTo(32).priority(.high)
}
versionControl.snp.makeConstraints { make in
make.width.equalTo(124)
make.height.equalTo(32)
}
highResolutionButton.snp.makeConstraints { make in
make.width.equalTo(112)
make.height.equalTo(32)
}
actionStack.snp.makeConstraints { make in
make.height.equalTo(58)
}
imageView.snp.makeConstraints { make in
make.top.equalTo(previewHeaderView.snp.bottom)
make.leading.trailing.equalToSuperview()
make.bottom.equalTo(bottomBar.snp.top).offset(-20)
}
}
/// 使
@objc private func backTapped() {
closePreview()
}
///
private func closePreview() {
if let navigationController,
navigationController.viewControllers.first !== self {
navigationController.popViewController(animated: true)
} else {
dismiss(animated: true)
}
}
/// Kingfisher
private func loadImage(forceRefresh: Bool = false) {
guard let material = previewState.currentMaterial else { return }
if let assetName = material.bundledImageAssetName,
let bundledImage = UIImage(named: assetName) {
originalImagesByMaterialID[material.id] = bundledImage
originalImage = bundledImage
renderCurrentPhoto()
return
}
let imageURL = material.fileUrl.isEmpty ? material.coverUrl : material.fileUrl
guard let url = URL(string: imageURL), !imageURL.isEmpty else { return }
let options: KingfisherOptionsInfo = forceRefresh ? [.forceRefresh] : []
let materialID = material.id
KingfisherManager.shared.retrieveImage(with: url, options: options) { [weak self] result in
guard let self, case let .success(value) = result else { return }
Task { @MainActor in
self.originalImagesByMaterialID[materialID] = value.image
guard self.previewState.currentMaterial?.id == materialID else { return }
self.originalImage = value.image
self.renderCurrentPhoto()
}
}
}
///
@objc private func photoSwiped(_ gesture: UISwipeGestureRecognizer) {
let didMove: Bool
switch gesture.direction {
case .left:
didMove = previewState.moveNext()
case .right:
didMove = previewState.movePrevious()
default:
didMove = false
}
guard didMove else { return }
displayCurrentPhoto()
}
///
private func displayCurrentPhoto() {
guard let material = previewState.currentMaterial else { return }
originalImage = originalImagesByMaterialID[material.id]
updateNavigationTitle()
renderCurrentPhoto()
updateAIEditControls()
if originalImage == nil {
loadImage()
}
}
///
private func updateNavigationTitle() {
guard let material = previewState.currentMaterial else { return }
previewHeaderView.configure(
title: material.fileName.isEmpty ? "照片预览" : material.fileName,
subtitle: TravelAlbumDisplayFormatter.fileSizeText(material.fileSize),
trailingText: "\(previewState.currentIndex + 1)/\(previewState.materials.count)"
)
}
@objc private func aiEditTapped() {
guard let material = previewState.currentMaterial else { return }
let state = aiEditResultStore.state(for: material.id)
guard state.canStartEditing else { return }
guard let originalImage else {
showToast("照片加载中,请稍后再试")
return
}
let controller = TravelAlbumAIEditSheetViewController(sourceImage: originalImage, selectedPhotoCount: 1)
controller.onConfirm = { [weak self] selection in
guard let self else { return }
self.startAIEditing(
materialID: material.id,
sourceImage: originalImage,
selection: selection
)
}
present(controller, animated: true)
}
///
private func startAIEditing(
materialID: Int,
sourceImage: UIImage,
selection: TravelAlbumAIEditSelection
) {
let state = aiEditResultStore.state(for: materialID)
guard state.canStartEditing else { return }
let existingCoverTemplate = state.appliedCoverTemplateID.flatMap { templateID in
TravelAlbumCoverTemplate.defaultOptions.first(where: { $0.id == templateID })
}
aiEditResultStore.startProcessing(materialIDs: [materialID])
updateAIEditControls()
let refinedImage = TravelAlbumAIEditImageProcessor.render(
effect: selection.refinedPreset.effect,
source: sourceImage
)
let atmosphereImage = selection.atmosphereOption.map { option in
TravelAlbumAIEditImageProcessor.render(effect: option.effect, source: sourceImage)
}
let coverImage = existingCoverTemplate.map { template in
TravelAlbumCoverTemplateImageProcessor.render(template: template, source: refinedImage)
}
let resultStore = aiEditResultStore
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
// 使
if selection.refinedPreset.effect == .original,
selection.atmosphereOption == nil,
existingCoverTemplate == nil {
resultStore.restoreOriginal(materialID: materialID)
} else {
resultStore.complete(
materialID: materialID,
presetID: selection.refinedPreset.id,
atmosphereOptionID: selection.atmosphereOption?.id,
coverTemplateID: existingCoverTemplate?.id,
editedImage: refinedImage,
atmosphereImage: atmosphereImage,
coverImage: coverImage
)
}
guard let self else { return }
if self.previewState.currentMaterial?.id == materialID {
let message: String
if selection.refinedPreset.effect == .original,
selection.atmosphereOption == nil,
existingCoverTemplate == nil {
message = "已还原为原图"
} else if selection.atmosphereOption != nil {
message = "AI修图完成已生成 2 个结果"
} else {
message = "AI修图完成"
}
self.showToast(message)
}
}
}
///
private func renderCurrentPhoto() {
guard let material = previewState.currentMaterial else { return }
let state = aiEditResultStore.state(for: material.id)
let displayImage: UIImage?
switch state.displayMode {
case .original:
displayImage = originalImagesByMaterialID[material.id]
case .edited:
displayImage = aiEditResultStore.editedImage(for: material.id) ?? originalImagesByMaterialID[material.id]
case .atmosphere:
displayImage = aiEditResultStore.atmosphereImage(for: material.id)
?? aiEditResultStore.editedImage(for: material.id)
?? originalImagesByMaterialID[material.id]
case .cover:
displayImage = aiEditResultStore.coverImage(for: material.id)
?? aiEditResultStore.editedImage(for: material.id)
?? originalImagesByMaterialID[material.id]
}
UIView.transition(with: imageView, duration: 0.2, options: .transitionCrossDissolve) {
self.imageView.image = displayImage ?? UIImage(systemName: "photo")
self.imageView.tintColor = UIColor.white.withAlphaComponent(0.5)
}
}
/// AI
private func updateAIEditControls() {
guard let material = previewState.currentMaterial else { return }
let state = aiEditResultStore.state(for: material.id)
comparisonBar.isHidden = !state.hasEditedResult
updateVersionControl(for: state)
aiEditButton.isEnabled = state.canStartEditing
aiEditButton.alpha = state.canStartEditing ? 1 : 0.35
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}
}
@objc private func versionChanged() {
guard let material = previewState.currentMaterial,
visibleDisplayModes.indices.contains(versionControl.selectedSegmentIndex) else { return }
aiEditResultStore.selectDisplayMode(
visibleDisplayModes[versionControl.selectedSegmentIndex],
materialID: material.id
)
renderCurrentPhoto()
}
///
private func updateVersionControl(for state: TravelAlbumAIEditResultState) {
var modes: [TravelAlbumAIEditResultState.DisplayMode] = [.original, .edited]
if state.hasAtmosphereResult {
modes.append(.atmosphere)
}
if state.isCover {
modes.append(.cover)
}
if visibleDisplayModes != modes {
visibleDisplayModes = modes
versionControl.removeAllSegments()
modes.enumerated().forEach { index, mode in
let title: String
switch mode {
case .original: title = "原图"
case .edited: title = "精修后"
case .atmosphere: title = "氛围感"
case .cover: title = "封面"
}
versionControl.insertSegment(withTitle: title, at: index, animated: false)
}
let width: CGFloat
switch modes.count {
case 4: width = 220
case 3: width = 180
default: width = 124
}
versionControl.snp.updateConstraints { make in
make.width.equalTo(width)
}
}
versionControl.selectedSegmentIndex = visibleDisplayModes.firstIndex(of: state.displayMode) ?? 1
}
@objc private func aiEditResultDidChange(_ notification: Notification) {
guard let materialID = notification.userInfo?[TravelAlbumAIEditResultStore.materialIDUserInfoKey] as? Int,
previewState.currentMaterial?.id == materialID else { return }
renderCurrentPhoto()
updateAIEditControls()
}
@objc private func highResolutionTapped() {
guard let image = imageView.image else { return }
let controller = TravelAlbumImagePreviewViewController(image: image, title: "高清图")
controller.modalPresentationStyle = .fullScreen
controller.modalTransitionStyle = .crossDissolve
present(controller, animated: true)
}
@objc private func deleteTapped() {
guard let material = previewState.currentMaterial else { return }
guard !material.isPurchased else {
showToast("已购照片不可删除")
return
}
let alert = UIAlertController(title: "删除照片", message: "删除后无法恢复,确定删除这张照片吗?", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "删除", style: .destructive) { [weak self] _ in
guard let self else { return }
Task {
do {
try await self.api.deleteMaterial(id: material.id)
await MainActor.run {
self.originalImagesByMaterialID.removeValue(forKey: material.id)
self.aiEditResultStore.remove(materialID: material.id)
self.onDeleted?()
if self.previewState.removeCurrent() {
self.displayCurrentPhoto()
} else {
self.closePreview()
}
}
} catch {
await MainActor.run {
self.showToast(error.localizedDescription.isEmpty ? "删除失败" : error.localizedDescription)
}
}
}
})
present(alert, animated: true)
}
@objc private func refreshTapped() {
guard let material = previewState.currentMaterial else { return }
imageView.image = UIImage(systemName: "arrow.triangle.2.circlepath")
imageView.tintColor = UIColor.white.withAlphaComponent(0.5)
originalImage = nil
originalImagesByMaterialID.removeValue(forKey: material.id)
loadImage(forceRefresh: true)
}
private func showToast(_ message: String) {
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
present(alert, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { [weak alert] in
alert?.dismiss(animated: true)
}
}
}
///
private final class TravelAlbumPreviewActionButton: UIButton {
init(title: String, icon: UIImage?) {
super.init(frame: .zero)
var configuration = UIButton.Configuration.plain()
configuration.image = icon
configuration.title = title
configuration.imagePlacement = .top
configuration.imagePadding = 6
configuration.baseForegroundColor = .white
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
var attributes = attributes
attributes.font = .systemFont(ofSize: 13)
return attributes
}
self.configuration = configuration
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
private final class TravelAlbumPreviewHeaderView: UIView {
var onBack: (() -> Void)?
private let backButton = UIButton(type: .system)
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let titleStack = UIStackView()
private let trailingLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func configure(title: String, subtitle: String?, trailingText: String?) {
titleLabel.text = title
subtitleLabel.text = subtitle
subtitleLabel.isHidden = subtitle?.isEmpty != false
trailingLabel.text = trailingText
trailingLabel.isHidden = trailingText?.isEmpty != false
}
private func setupUI() {
backgroundColor = .black
var backConfiguration = UIButton.Configuration.plain()
backConfiguration.image = UIImage(
systemName: "chevron.left",
withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .semibold)
)
backConfiguration.baseForegroundColor = .white
backConfiguration.contentInsets = .zero
backButton.configuration = backConfiguration
backButton.backgroundColor = UIColor.white.withAlphaComponent(0.10)
backButton.layer.cornerRadius = 20
backButton.accessibilityLabel = "返回"
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
titleLabel.textColor = .white
titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
titleLabel.textAlignment = .center
titleLabel.lineBreakMode = .byTruncatingMiddle
subtitleLabel.textColor = UIColor.white.withAlphaComponent(0.55)
subtitleLabel.font = .systemFont(ofSize: 12, weight: .regular)
subtitleLabel.textAlignment = .center
titleStack.axis = .vertical
titleStack.alignment = .fill
titleStack.spacing = 3
titleStack.addArrangedSubview(titleLabel)
titleStack.addArrangedSubview(subtitleLabel)
trailingLabel.textColor = UIColor.white.withAlphaComponent(0.78)
trailingLabel.font = .monospacedDigitSystemFont(ofSize: 12, weight: .medium)
trailingLabel.textAlignment = .center
trailingLabel.backgroundColor = UIColor.white.withAlphaComponent(0.10)
trailingLabel.layer.cornerRadius = 14
trailingLabel.clipsToBounds = true
addSubview(backButton)
addSubview(titleStack)
addSubview(trailingLabel)
backButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.centerY.equalToSuperview()
make.width.height.equalTo(40)
}
trailingLabel.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-16)
make.centerY.equalToSuperview()
make.width.greaterThanOrEqualTo(44)
make.height.equalTo(28)
}
titleStack.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.greaterThanOrEqualTo(backButton.snp.trailing).offset(12)
make.trailing.lessThanOrEqualTo(trailingLabel.snp.leading).offset(-12)
make.width.lessThanOrEqualTo(230)
}
}
@objc private func backTapped() {
onBack?()
}
}
/// 沿
private final class TravelAlbumImagePreviewViewController: UIViewController, UIScrollViewDelegate {
private let image: UIImage
private let previewTitle: String
private let previewHeaderView = TravelAlbumPreviewHeaderView()
private let scrollView = UIScrollView()
private let imageView = UIImageView()
init(image: UIImage, title: String = "高清图") {
self.image = image
previewTitle = title
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
previewHeaderView.configure(
title: previewTitle,
subtitle: "双指缩放查看细节",
trailingText: nil
)
previewHeaderView.onBack = { [weak self] in
self?.dismiss(animated: true)
}
scrollView.delegate = self
scrollView.minimumZoomScale = 1
scrollView.maximumZoomScale = 4
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
imageView.image = image
imageView.contentMode = .scaleAspectFit
view.addSubview(previewHeaderView)
view.addSubview(scrollView)
scrollView.addSubview(imageView)
previewHeaderView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide)
make.leading.trailing.equalToSuperview()
make.height.equalTo(68)
}
scrollView.snp.makeConstraints { make in
make.top.equalTo(previewHeaderView.snp.bottom)
make.leading.trailing.bottom.equalToSuperview()
}
imageView.snp.makeConstraints { make in
make.edges.equalTo(scrollView.contentLayoutGuide)
make.width.height.equalTo(scrollView.frameLayoutGuide)
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
imageView
}
override var preferredStatusBarStyle: UIStatusBarStyle {
.lightContent
}
}
/// AI
final class TravelAlbumAIEditSheetViewController: UIViewController {
var onConfirm: ((TravelAlbumAIEditSelection) -> Void)?
private let sourceImage: UIImage?
private let selectedPhotoCount: Int
private let presets = TravelAlbumEditPreset.defaultOptions
private let atmosphereOptions = TravelAlbumAtmosphereEditOption.defaultOptions
private let coverTemplates = TravelAlbumCoverTemplate.defaultOptions
private let showsCoverTemplates: Bool
private var selectedPresetID = TravelAlbumEditPreset.defaultOptions[0].id
private var selectedAtmosphereOptionID: String?
private var selectedCoverTemplateID: String?
private let scrollView = UIScrollView()
private let contentView = UIView()
private let tipsContainerView = UIView()
private let tipsLabel = UILabel()
private let quotaInfoView = UIView()
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Int, TravelAlbumEditPreset>!
private var atmosphereCollectionView: UICollectionView!
private var atmosphereDataSource: UICollectionViewDiffableDataSource<Int, TravelAlbumAtmosphereEditOption>!
private var coverCollectionView: UICollectionView!
private var coverDataSource: UICollectionViewDiffableDataSource<Int, TravelAlbumCoverTemplate>!
init(sourceImage: UIImage?, selectedPhotoCount: Int) {
self.sourceImage = sourceImage
self.selectedPhotoCount = max(selectedPhotoCount, 1)
showsCoverTemplates = TravelAlbumCoverTemplate.shouldShow(for: selectedPhotoCount)
selectedCoverTemplateID = showsCoverTemplates ? TravelAlbumCoverTemplate.defaultOptions.first?.id : nil
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .pageSheet
if let sheetPresentationController {
let contentIdentifier = UISheetPresentationController.Detent.Identifier("aiEditContent")
let desiredHeight: CGFloat = showsCoverTemplates ? 920 : 770
let contentDetent = UISheetPresentationController.Detent.custom(identifier: contentIdentifier) { context in
return min(desiredHeight, context.maximumDetentValue)
}
sheetPresentationController.detents = [contentDetent, .large()]
sheetPresentationController.selectedDetentIdentifier = contentIdentifier
sheetPresentationController.prefersGrabberVisible = true
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = AppColor.pageBackground
setupUI()
applySnapshot()
}
private func setupUI() {
let titleLabel = UILabel()
titleLabel.text = "AI修图"
titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
titleLabel.textColor = AppColor.textPrimary
titleLabel.textAlignment = .center
let countLabel = UILabel()
countLabel.text = "已选择 \(selectedPhotoCount) 张照片"
countLabel.font = .systemFont(ofSize: 15)
countLabel.textColor = AppColor.textSecondary
countLabel.textAlignment = .center
tipsContainerView.backgroundColor = UIColor(hex: 0xEAF2FF)
tipsContainerView.layer.cornerRadius = 10
tipsLabel.font = .systemFont(ofSize: 13)
tipsLabel.textColor = UIColor(hex: 0x43617F)
tipsLabel.numberOfLines = 0
tipsContainerView.addSubview(tipsLabel)
tipsLabel.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(12)
}
let presetTitleLabel = UILabel()
presetTitleLabel.text = "原图精修"
presetTitleLabel.font = .systemFont(ofSize: 15, weight: .semibold)
presetTitleLabel.textColor = AppColor.textPrimary
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 12
layout.itemSize = CGSize(width: 116, height: 142)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = .clear
collectionView.showsHorizontalScrollIndicator = false
collectionView.delegate = self
collectionView.register(TravelAlbumEditPresetCell.self, forCellWithReuseIdentifier: TravelAlbumEditPresetCell.reuseIdentifier)
dataSource = UICollectionViewDiffableDataSource<Int, TravelAlbumEditPreset>(collectionView: collectionView) { [weak self] collectionView, indexPath, preset in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TravelAlbumEditPresetCell.reuseIdentifier, for: indexPath) as! TravelAlbumEditPresetCell
cell.apply(preset: preset, sourceImage: self?.sourceImage, selected: self?.selectedPresetID == preset.id)
cell.onPreview = { [weak self] in
guard let self else { return }
let previewImage = self.sourceImage.map {
TravelAlbumAIEditImageProcessor.render(effect: preset.effect, source: $0)
}
self.presentStylePreview(title: preset.title, image: previewImage)
}
return cell
}
let atmosphereHeaderView = UIView()
let atmosphereTitleLabel = UILabel()
atmosphereTitleLabel.text = "氛围感修图"
atmosphereTitleLabel.font = .systemFont(ofSize: 15, weight: .semibold)
atmosphereTitleLabel.textColor = AppColor.textPrimary
let optionalLabel = UILabel()
optionalLabel.text = "选填"
optionalLabel.font = .systemFont(ofSize: 12, weight: .medium)
optionalLabel.textColor = AppColor.primary
optionalLabel.backgroundColor = UIColor(hex: 0xEAF2FF)
optionalLabel.textAlignment = .center
optionalLabel.layer.cornerRadius = 13
optionalLabel.clipsToBounds = true
atmosphereHeaderView.addSubview(atmosphereTitleLabel)
atmosphereHeaderView.addSubview(optionalLabel)
atmosphereTitleLabel.snp.makeConstraints { make in
make.leading.centerY.equalToSuperview()
}
optionalLabel.snp.makeConstraints { make in
make.leading.equalTo(atmosphereTitleLabel.snp.trailing).offset(8)
make.centerY.equalToSuperview()
make.width.equalTo(48)
make.height.equalTo(26)
}
let atmosphereLayout = UICollectionViewFlowLayout()
atmosphereLayout.scrollDirection = .horizontal
atmosphereLayout.minimumLineSpacing = 12
atmosphereLayout.itemSize = CGSize(width: 116, height: 142)
atmosphereCollectionView = UICollectionView(frame: .zero, collectionViewLayout: atmosphereLayout)
atmosphereCollectionView.backgroundColor = .clear
atmosphereCollectionView.showsHorizontalScrollIndicator = false
atmosphereCollectionView.delegate = self
atmosphereCollectionView.register(
TravelAlbumEditPresetCell.self,
forCellWithReuseIdentifier: TravelAlbumEditPresetCell.reuseIdentifier
)
atmosphereDataSource = UICollectionViewDiffableDataSource<Int, TravelAlbumAtmosphereEditOption>(
collectionView: atmosphereCollectionView
) { [weak self] collectionView, indexPath, option in
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: TravelAlbumEditPresetCell.reuseIdentifier,
for: indexPath
) as! TravelAlbumEditPresetCell
cell.apply(
atmosphereOption: option,
sourceImage: self?.sourceImage,
selected: self?.selectedAtmosphereOptionID == option.id
)
cell.onPreview = { [weak self] in
guard let self else { return }
let previewImage = self.sourceImage.map {
TravelAlbumAIEditImageProcessor.render(effect: option.effect, source: $0)
}
self.presentStylePreview(title: option.title, image: previewImage)
}
return cell
}
updateAtmosphereSelection()
let coverHeaderView = UIView()
coverHeaderView.isHidden = !showsCoverTemplates
let coverTitleLabel = UILabel()
coverTitleLabel.text = "封面风格模板"
coverTitleLabel.font = .systemFont(ofSize: 15, weight: .semibold)
coverTitleLabel.textColor = AppColor.textPrimary
let freeGiftLabel = UILabel()
freeGiftLabel.text = "赠送 · 不占精修张数"
freeGiftLabel.font = .systemFont(ofSize: 12, weight: .medium)
freeGiftLabel.textColor = UIColor(hex: 0xF29A38)
freeGiftLabel.backgroundColor = UIColor(hex: 0xFFF3DE)
freeGiftLabel.textAlignment = .center
freeGiftLabel.layer.cornerRadius = 13
freeGiftLabel.clipsToBounds = true
coverHeaderView.addSubview(coverTitleLabel)
coverHeaderView.addSubview(freeGiftLabel)
coverTitleLabel.snp.makeConstraints { make in
make.leading.centerY.equalToSuperview()
}
freeGiftLabel.snp.makeConstraints { make in
make.trailing.centerY.equalToSuperview()
make.height.equalTo(26)
make.width.equalTo(148)
}
let coverLayout = UICollectionViewFlowLayout()
coverLayout.scrollDirection = .horizontal
coverLayout.minimumLineSpacing = 12
coverLayout.itemSize = CGSize(width: 116, height: 142)
coverCollectionView = UICollectionView(frame: .zero, collectionViewLayout: coverLayout)
coverCollectionView.backgroundColor = .clear
coverCollectionView.showsHorizontalScrollIndicator = false
coverCollectionView.isHidden = !showsCoverTemplates
coverCollectionView.delegate = self
coverCollectionView.register(
TravelAlbumCoverTemplateCell.self,
forCellWithReuseIdentifier: TravelAlbumCoverTemplateCell.reuseIdentifier
)
coverDataSource = UICollectionViewDiffableDataSource<Int, TravelAlbumCoverTemplate>(
collectionView: coverCollectionView
) { [weak self] collectionView, indexPath, template in
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: TravelAlbumCoverTemplateCell.reuseIdentifier,
for: indexPath
) as! TravelAlbumCoverTemplateCell
cell.apply(
template: template,
sourceImage: self?.sourceImage,
selected: self?.selectedCoverTemplateID == template.id
)
cell.onPreview = { [weak self] in
guard let self else { return }
let previewImage = TravelAlbumCoverTemplateImageProcessor.render(
template: template,
source: self.sourceImage
)
self.presentStylePreview(title: template.title, image: previewImage)
}
return cell
}
let modeTitleLabel = UILabel()
modeTitleLabel.text = "修图模式"
modeTitleLabel.font = .systemFont(ofSize: 15)
modeTitleLabel.textColor = AppColor.textSecondary
quotaInfoView.backgroundColor = .white
quotaInfoView.layer.cornerRadius = 12
quotaInfoView.accessibilityLabel = "AI精修按张收费剩余9张"
quotaInfoView.accessibilityTraits = .staticText
let modeLabel = UILabel()
modeLabel.text = "AI精修 · 按张收费"
modeLabel.font = .systemFont(ofSize: 16, weight: .medium)
modeLabel.textColor = AppColor.textPrimary
let quotaLabel = UILabel()
quotaLabel.text = "剩余 9 张"
quotaLabel.font = .systemFont(ofSize: 14, weight: .medium)
quotaLabel.textColor = AppColor.primary
quotaLabel.backgroundColor = UIColor(hex: 0xEAF2FF)
quotaLabel.textAlignment = .center
quotaLabel.layer.cornerRadius = 15
quotaLabel.clipsToBounds = true
quotaInfoView.addSubview(modeLabel)
quotaInfoView.addSubview(quotaLabel)
modeLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.centerY.equalToSuperview()
}
quotaLabel.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-16)
make.centerY.equalToSuperview()
make.width.equalTo(82)
make.height.equalTo(30)
}
let cancelButton = makeBottomButton(title: "取消", isPrimary: false)
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
let confirmButton = makeBottomButton(title: "确定", isPrimary: true)
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
let buttonStack = UIStackView(arrangedSubviews: [cancelButton, confirmButton])
buttonStack.axis = .horizontal
buttonStack.spacing = 12
buttonStack.distribution = .fillEqually
view.addSubview(scrollView)
view.addSubview(buttonStack)
scrollView.addSubview(contentView)
[
titleLabel,
countLabel,
tipsContainerView,
presetTitleLabel,
collectionView,
atmosphereHeaderView,
atmosphereCollectionView,
coverHeaderView,
coverCollectionView,
modeTitleLabel,
quotaInfoView,
].forEach(contentView.addSubview)
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = showsCoverTemplates
scrollView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.bottom.equalTo(buttonStack.snp.top).offset(-12)
}
contentView.snp.makeConstraints { make in
make.edges.equalTo(scrollView.contentLayoutGuide)
make.width.equalTo(scrollView.frameLayoutGuide)
}
buttonStack.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(20)
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-16)
make.height.equalTo(48)
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(28)
make.leading.trailing.equalToSuperview().inset(24)
}
countLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(12)
make.leading.trailing.equalTo(titleLabel)
}
tipsContainerView.snp.makeConstraints { make in
make.top.equalTo(countLabel.snp.bottom).offset(14)
make.leading.trailing.equalToSuperview().inset(20)
}
presetTitleLabel.snp.makeConstraints { make in
make.top.equalTo(tipsContainerView.snp.bottom).offset(20)
make.leading.equalToSuperview().offset(20)
}
collectionView.snp.makeConstraints { make in
make.top.equalTo(presetTitleLabel.snp.bottom).offset(12)
make.leading.trailing.equalToSuperview().inset(20)
make.height.equalTo(142)
}
atmosphereHeaderView.snp.makeConstraints { make in
make.top.equalTo(collectionView.snp.bottom).offset(20)
make.leading.trailing.equalToSuperview().inset(20)
make.height.equalTo(26)
}
atmosphereCollectionView.snp.makeConstraints { make in
make.top.equalTo(atmosphereHeaderView.snp.bottom).offset(12)
make.leading.trailing.equalToSuperview().inset(20)
make.height.equalTo(142)
}
coverHeaderView.snp.makeConstraints { make in
make.top.equalTo(atmosphereCollectionView.snp.bottom).offset(showsCoverTemplates ? 20 : 0)
make.leading.trailing.equalToSuperview().inset(20)
make.height.equalTo(showsCoverTemplates ? 26 : 0)
}
coverCollectionView.snp.makeConstraints { make in
make.top.equalTo(coverHeaderView.snp.bottom).offset(showsCoverTemplates ? 12 : 0)
make.leading.trailing.equalToSuperview().inset(20)
make.height.equalTo(showsCoverTemplates ? 142 : 0)
}
modeTitleLabel.snp.makeConstraints { make in
make.top.equalTo(coverCollectionView.snp.bottom).offset(20)
make.leading.equalToSuperview().offset(20)
}
quotaInfoView.snp.makeConstraints { make in
make.top.equalTo(modeTitleLabel.snp.bottom).offset(12)
make.leading.trailing.equalToSuperview().inset(20)
make.height.equalTo(62)
make.bottom.equalToSuperview().offset(-20)
}
}
private func makeBottomButton(title: String, isPrimary: Bool) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
button.setTitleColor(isPrimary ? .white : AppColor.textPrimary, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
button.backgroundColor = isPrimary ? AppColor.primary : .white
button.layer.cornerRadius = 10
button.layer.borderWidth = isPrimary ? 0 : 1
button.layer.borderColor = UIColor(hex: 0xD5D5D5).cgColor
return button
}
private func applySnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Int, TravelAlbumEditPreset>()
snapshot.appendSections([0])
snapshot.appendItems(presets)
dataSource.apply(snapshot, animatingDifferences: false)
var atmosphereSnapshot = NSDiffableDataSourceSnapshot<Int, TravelAlbumAtmosphereEditOption>()
atmosphereSnapshot.appendSections([0])
atmosphereSnapshot.appendItems(atmosphereOptions)
atmosphereDataSource.apply(atmosphereSnapshot, animatingDifferences: false)
guard showsCoverTemplates else { return }
var coverSnapshot = NSDiffableDataSourceSnapshot<Int, TravelAlbumCoverTemplate>()
coverSnapshot.appendSections([0])
coverSnapshot.appendItems(coverTemplates)
coverDataSource.apply(coverSnapshot, animatingDifferences: false)
}
@objc private func cancelTapped() {
dismiss(animated: true)
}
/// Tips
private func updateAtmosphereSelection() {
let isAtmosphereSelected = selectedAtmosphereOptionID != nil
if showsCoverTemplates {
tipsLabel.text = isAtmosphereSelected
? "Tips本次显示 3 类风格。每张照片生成「精修后 + 氛围感」2 个结果,第一张照片另生成所选封面结果。"
: "Tips氛围感修图为选填可横向选择一种样式选中后每张照片会额外生成 1 个独立结果,第一张照片仍另生成封面。"
} else {
tipsLabel.text = isAtmosphereSelected
? "Tips已同时选择原图精修和氛围感修图每张照片会生成「精修后 + 氛围感」2 个独立结果。"
: "Tips原图精修、氛围感修图为两类独立模板同时选择这两种模板时同一张原图将并行生成两份不同的修图成品。"
}
}
///
private func presentStylePreview(title: String, image: UIImage?) {
guard let image else {
let alert = UIAlertController(
title: "暂时无法预览",
message: "当前照片仍在加载,请稍后再试。",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "知道了", style: .default))
present(alert, animated: true)
return
}
let controller = TravelAlbumImagePreviewViewController(image: image, title: title)
controller.modalPresentationStyle = .fullScreen
controller.modalTransitionStyle = .crossDissolve
present(controller, animated: true)
}
@objc private func confirmTapped() {
guard let preset = presets.first(where: { $0.id == selectedPresetID }) else { return }
let coverTemplate = selectedCoverTemplateID.flatMap { selectedID in
coverTemplates.first(where: { $0.id == selectedID })
}
let atmosphereOption = selectedAtmosphereOptionID.flatMap { selectedID in
atmosphereOptions.first(where: { $0.id == selectedID })
}
let selection = TravelAlbumAIEditSelection(
refinedPreset: preset,
atmosphereOption: atmosphereOption,
coverTemplate: coverTemplate
)
onConfirm?(selection)
dismiss(animated: true)
}
}
extension TravelAlbumAIEditSheetViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if collectionView === coverCollectionView {
guard let template = coverDataSource.itemIdentifier(for: indexPath) else { return }
selectedCoverTemplateID = template.id
collectionView.visibleCells.compactMap { $0 as? TravelAlbumCoverTemplateCell }.forEach { cell in
guard let visiblePath = collectionView.indexPath(for: cell),
let visibleTemplate = coverDataSource.itemIdentifier(for: visiblePath) else { return }
cell.setSelected(visibleTemplate.id == selectedCoverTemplateID)
}
return
}
if collectionView === atmosphereCollectionView {
guard let option = atmosphereDataSource.itemIdentifier(for: indexPath) else { return }
//
selectedAtmosphereOptionID = selectedAtmosphereOptionID == option.id ? nil : option.id
collectionView.visibleCells.compactMap { $0 as? TravelAlbumEditPresetCell }.forEach { cell in
guard let visiblePath = collectionView.indexPath(for: cell),
let visibleOption = atmosphereDataSource.itemIdentifier(for: visiblePath) else { return }
cell.setSelected(visibleOption.id == selectedAtmosphereOptionID)
}
updateAtmosphereSelection()
return
}
guard let preset = dataSource.itemIdentifier(for: indexPath) else { return }
selectedPresetID = preset.id
collectionView.visibleCells.compactMap { $0 as? TravelAlbumEditPresetCell }.forEach { cell in
guard let visiblePath = collectionView.indexPath(for: cell), let visiblePreset = dataSource.itemIdentifier(for: visiblePath) else { return }
cell.setSelected(visiblePreset.id == selectedPresetID)
}
}
}
///
private final class TravelAlbumEditPresetCell: UICollectionViewCell {
static let reuseIdentifier = "TravelAlbumEditPresetCell"
var onPreview: (() -> Void)?
private let imageView = UIImageView()
private let titleLabel = UILabel()
private let selectionView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
private let previewButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .white
contentView.layer.cornerRadius = 10
contentView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.backgroundColor = UIColor(hex: 0xEAF2FF)
titleLabel.font = .systemFont(ofSize: 12, weight: .medium)
titleLabel.textColor = AppColor.textPrimary
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 2
selectionView.tintColor = AppColor.primary
selectionView.backgroundColor = .white
selectionView.layer.cornerRadius = 10
selectionView.clipsToBounds = true
previewButton.setTitle("预览", for: .normal)
previewButton.setTitleColor(.white, for: .normal)
previewButton.titleLabel?.font = .systemFont(ofSize: 11, weight: .medium)
previewButton.backgroundColor = UIColor.black.withAlphaComponent(0.58)
previewButton.layer.cornerRadius = 12
previewButton.addTarget(self, action: #selector(previewTapped), for: .touchUpInside)
contentView.addSubview(imageView)
contentView.addSubview(titleLabel)
contentView.addSubview(selectionView)
contentView.addSubview(previewButton)
imageView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(96)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(imageView.snp.bottom).offset(6)
make.leading.trailing.equalToSuperview().inset(5)
make.bottom.lessThanOrEqualToSuperview().offset(-5)
}
selectionView.snp.makeConstraints { make in
make.top.trailing.equalToSuperview().inset(6)
make.size.equalTo(20)
}
previewButton.snp.makeConstraints { make in
make.trailing.bottom.equalTo(imageView).inset(6)
make.width.equalTo(46)
make.height.equalTo(24)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(preset: TravelAlbumEditPreset, sourceImage: UIImage?, selected: Bool) {
apply(
title: preset.title,
effect: preset.effect,
sourceImage: sourceImage,
selected: selected
)
}
///
func apply(
atmosphereOption: TravelAlbumAtmosphereEditOption,
sourceImage: UIImage?,
selected: Bool
) {
apply(
title: atmosphereOption.title,
effect: atmosphereOption.effect,
sourceImage: sourceImage,
selected: selected
)
accessibilityHint = "再次点击已选样式可取消,点击预览按钮查看大图"
}
private func apply(
title: String,
effect: TravelAlbumEditPreset.Effect,
sourceImage: UIImage?,
selected: Bool
) {
titleLabel.text = title
imageView.image = sourceImage.map {
TravelAlbumAIEditImageProcessor.render(effect: effect, source: $0)
} ?? UIImage(systemName: "photo")
setSelected(selected)
accessibilityLabel = title
accessibilityValue = selected ? "已选中" : "未选中"
}
func setSelected(_ selected: Bool) {
contentView.layer.borderWidth = selected ? 2 : 0
contentView.layer.borderColor = selected ? AppColor.primary.cgColor : UIColor.clear.cgColor
selectionView.isHidden = !selected
accessibilityValue = selected ? "已选中" : "未选中"
}
override func prepareForReuse() {
super.prepareForReuse()
onPreview = nil
accessibilityHint = nil
}
@objc private func previewTapped() {
onPreview?()
}
}
///
private final class TravelAlbumCoverTemplateCell: UICollectionViewCell {
static let reuseIdentifier = "TravelAlbumCoverTemplateCell"
var onPreview: (() -> Void)?
private let imageView = UIImageView()
private let titleLabel = UILabel()
private let selectionView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
private let previewButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .white
contentView.layer.cornerRadius = 10
contentView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.backgroundColor = UIColor(hex: 0xEAF2FF)
titleLabel.font = .systemFont(ofSize: 12, weight: .medium)
titleLabel.textColor = AppColor.textPrimary
titleLabel.textAlignment = .center
selectionView.tintColor = AppColor.primary
selectionView.backgroundColor = .white
selectionView.layer.cornerRadius = 10
selectionView.clipsToBounds = true
previewButton.setTitle("预览", for: .normal)
previewButton.setTitleColor(.white, for: .normal)
previewButton.titleLabel?.font = .systemFont(ofSize: 11, weight: .medium)
previewButton.backgroundColor = UIColor.black.withAlphaComponent(0.58)
previewButton.layer.cornerRadius = 12
previewButton.addTarget(self, action: #selector(previewTapped), for: .touchUpInside)
contentView.addSubview(imageView)
contentView.addSubview(titleLabel)
contentView.addSubview(selectionView)
contentView.addSubview(previewButton)
imageView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(104)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(imageView.snp.bottom).offset(6)
make.leading.trailing.equalToSuperview().inset(5)
make.bottom.lessThanOrEqualToSuperview().offset(-5)
}
selectionView.snp.makeConstraints { make in
make.top.trailing.equalToSuperview().inset(6)
make.size.equalTo(20)
}
previewButton.snp.makeConstraints { make in
make.trailing.bottom.equalTo(imageView).inset(6)
make.width.equalTo(46)
make.height.equalTo(24)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func apply(template: TravelAlbumCoverTemplate, sourceImage: UIImage?, selected: Bool) {
titleLabel.text = template.title
imageView.image = TravelAlbumCoverTemplateImageProcessor.render(
template: template,
source: sourceImage,
targetSize: CGSize(width: 232, height: 208)
)
setSelected(selected)
accessibilityLabel = "\(template.title)封面模板"
accessibilityValue = selected ? "已选中" : "未选中"
accessibilityHint = "点击预览按钮查看完整模板样式"
}
///
func setSelected(_ selected: Bool) {
contentView.layer.borderWidth = selected ? 2 : 0
contentView.layer.borderColor = selected ? AppColor.primary.cgColor : UIColor.clear.cgColor
selectionView.isHidden = !selected
accessibilityValue = selected ? "已选中" : "未选中"
}
override func prepareForReuse() {
super.prepareForReuse()
onPreview = nil
}
@objc private func previewTapped() {
onPreview?()
}
}