Files
suixinkan_uikit/suixinkan/UI/WildPhotographerReport/WildReportRiskMapViewController.swift

757 lines
28 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.

//
// WildReportRiskMapViewController.swift
// suixinkan
//
import Kingfisher
import MapKit
import SnapKit
import UIKit
/// 线
final class WildReportRiskMapViewController: BaseViewController {
private let viewModel: WildReportRiskMapViewModel
private let api: any WildPhotographerReportServing
private let locationProvider: any LocationProviding
private let scenicIdProvider: () -> Int
private let scenicNameProvider: () -> String
private let rootStack = UIStackView()
private let mapContainerView = UIView()
private let mapView = MKMapView()
private let scenicCapsule = UIView()
private let scenicNameLabel = UILabel()
private let legendBar = UIStackView()
private let detailContainerView = UIView()
private let scrollView = UIScrollView()
private let contentStack = UIStackView()
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
private var annotationMap: [String: WildReportMapMarker] = [:]
init(
record: WildReportRecord,
riskPoints: [WildReportRiskPoint],
api: any WildPhotographerReportServing = NetworkServices.shared.wildPhotographerReportAPI,
locationProvider: any LocationProviding = LocationProvider.shared,
scenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId },
scenicNameProvider: @escaping () -> String = { AppStore.shared.currentScenicName }
) {
self.viewModel = WildReportRiskMapViewModel(record: record, riskPoints: riskPoints)
self.api = api
self.locationProvider = locationProvider
self.scenicIdProvider = scenicIdProvider
self.scenicNameProvider = scenicNameProvider
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
Task {
await viewModel.loadInitial(
api: api,
locationProvider: locationProvider,
scenicId: scenicIdProvider(),
scenicName: scenicNameProvider()
)
}
}
override func setupNavigationBar() {
title = "附近风险地图"
}
override func setupUI() {
view.backgroundColor = UIColor(hex: 0xF8FAFF)
rootStack.axis = .vertical
rootStack.spacing = 0
view.addSubview(rootStack)
mapContainerView.backgroundColor = UIColor(hex: 0xE8F3FF)
mapContainerView.clipsToBounds = true
rootStack.addArrangedSubview(mapContainerView)
mapView.delegate = self
mapView.showsCompass = false
mapView.showsScale = true
mapView.pointOfInterestFilter = .excludingAll
mapContainerView.addSubview(mapView)
configureScenicCapsule()
configureLegendBar()
configureDetailArea()
refreshContent(animated: false)
}
override func setupConstraints() {
rootStack.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
mapContainerView.snp.makeConstraints { make in
make.height.equalTo(340)
}
mapView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
scenicCapsule.snp.makeConstraints { make in
make.top.leading.equalToSuperview().inset(12)
make.height.equalTo(34)
}
detailContainerView.snp.makeConstraints { make in
make.height.greaterThanOrEqualTo(180)
}
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in
self?.refreshContent(animated: true)
}
}
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
}
private func configureScenicCapsule() {
scenicCapsule.backgroundColor = UIColor.white.withAlphaComponent(0.94)
scenicCapsule.layer.cornerRadius = 17
scenicCapsule.layer.shadowColor = UIColor.black.cgColor
scenicCapsule.layer.shadowOpacity = 0.08
scenicCapsule.layer.shadowRadius = 8
scenicCapsule.layer.shadowOffset = CGSize(width: 0, height: 4)
let icon = UIImageView(image: UIImage(systemName: "mountain.2.fill"))
icon.tintColor = AppColor.primary
icon.contentMode = .scaleAspectFit
scenicNameLabel.font = .systemFont(ofSize: 13, weight: .semibold)
scenicNameLabel.textColor = UIColor(hex: 0x195591)
mapContainerView.addSubview(scenicCapsule)
scenicCapsule.addSubview(icon)
scenicCapsule.addSubview(scenicNameLabel)
icon.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(12)
make.centerY.equalToSuperview()
make.size.equalTo(16)
}
scenicNameLabel.snp.makeConstraints { make in
make.leading.equalTo(icon.snp.trailing).offset(6)
make.trailing.equalToSuperview().inset(12)
make.centerY.equalToSuperview()
}
}
private func configureLegendBar() {
legendBar.axis = .horizontal
legendBar.alignment = .center
legendBar.distribution = .fillEqually
legendBar.spacing = 6
legendBar.backgroundColor = .white
legendBar.layoutMargins = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
legendBar.isLayoutMarginsRelativeArrangement = true
rootStack.addArrangedSubview(legendBar)
rebuildLegend()
let divider = UIView()
divider.backgroundColor = AppColor.cardOutline
rootStack.addArrangedSubview(divider)
divider.snp.makeConstraints { make in
make.height.equalTo(1 / UIScreen.main.scale)
}
}
private func configureDetailArea() {
detailContainerView.backgroundColor = UIColor(hex: 0xF8FAFF)
rootStack.addArrangedSubview(detailContainerView)
detailContainerView.addSubview(scrollView)
scrollView.alwaysBounceVertical = true
scrollView.addSubview(contentStack)
contentStack.axis = .vertical
contentStack.spacing = 14
scrollView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
contentStack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 14, left: 18, bottom: 18, right: 18))
make.width.equalTo(scrollView.snp.width).offset(-36)
}
}
@MainActor
private func refreshContent(animated: Bool) {
scenicNameLabel.text = viewModel.scenicAreaName
syncMapAnnotations()
mapView.setRegion(viewModel.region, animated: animated)
updateAnnotationSelection()
rebuildLegend()
rebuildPanel()
}
private func syncMapAnnotations() {
let oldAnnotations = mapView.annotations.compactMap { $0 as? WildReportMapAnnotation }
mapView.removeAnnotations(oldAnnotations)
annotationMap.removeAll()
let annotations = viewModel.markers.map { marker in
annotationMap[marker.id] = marker
return WildReportMapAnnotation(marker: marker)
}
mapView.addAnnotations(annotations)
}
private func updateAnnotationSelection() {
for annotation in mapView.annotations {
guard let annotation = annotation as? WildReportMapAnnotation,
let view = mapView.view(for: annotation) as? WildReportPulseMarkerAnnotationView
else { continue }
view.apply(marker: annotation.marker, selected: annotation.marker.id == viewModel.selectedMarkerID)
}
}
private func rebuildLegend() {
legendBar.arrangedSubviews.forEach { view in
legendBar.removeArrangedSubview(view)
view.removeFromSuperview()
}
[
WildReportMapMarkerKind.selfLocation,
.wildPhotographer,
.processingClue,
.peerPhotographer
].forEach { kind in
legendBar.addArrangedSubview(WildReportRiskLegendView(kind: kind))
}
}
@MainActor
private func rebuildPanel() {
contentStack.arrangedSubviews.forEach { view in
contentStack.removeArrangedSubview(view)
view.removeFromSuperview()
}
if viewModel.isLoading {
contentStack.addArrangedSubview(makeLoadingView(text: "正在加载风险点位..."))
}
if let message = viewModel.errorMessage, !viewModel.isLoading {
contentStack.addArrangedSubview(makeStateCard(icon: "exclamationmark.triangle.fill", text: message))
}
guard let marker = viewModel.selectedMarker,
case .activeClue(let clue) = marker.detail
else {
contentStack.addArrangedSubview(makeStateCard(icon: "hand.tap.fill", text: "点击地图标记查看详情"))
return
}
let card = WildReportCardView(spacing: 16, inset: 16)
card.layer.cornerRadius = 16
card.stack.addArrangedSubview(makeHeader(marker: marker, clue: clue))
if viewModel.isLoadingDetail {
card.stack.addArrangedSubview(makeInlineLoading(text: "正在加载标记详情..."))
}
if let nearest = clue.nearestAbnormalText {
card.stack.addArrangedSubview(makeHighlight(text: nearest, color: AppColor.danger))
}
addInfoRows(for: clue, to: card.stack)
addDescriptionBlocks(for: clue, to: card.stack)
addMediaRow(for: clue, to: card.stack)
addProcessingBlock(for: clue, to: card.stack)
card.stack.addArrangedSubview(makeButtonRow(for: clue))
contentStack.addArrangedSubview(card)
}
private func makeHeader(marker: WildReportMapMarker, clue: WildReportRadarActiveClue) -> UIView {
let row = UIStackView()
row.axis = .horizontal
row.alignment = .center
row.spacing = 12
let iconWrap = UIView()
iconWrap.backgroundColor = marker.kind.color.withAlphaComponent(0.14)
iconWrap.layer.cornerRadius = 22
let icon = UIImageView(image: UIImage(systemName: iconName(for: marker.kind)))
icon.tintColor = marker.kind.color
icon.contentMode = .scaleAspectFit
iconWrap.addSubview(icon)
icon.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(22)
}
iconWrap.snp.makeConstraints { make in
make.size.equalTo(44)
}
let textStack = UIStackView()
textStack.axis = .vertical
textStack.spacing = 4
let title = WildReportUI.label(clue.displayTitle, font: .systemFont(ofSize: 17, weight: .bold), color: AppColor.textPrimary, lines: 2)
let metaText = [clue.distanceText ?? clue.distance, clue.updatedAt].compactMap { $0 }.joined(separator: " · ")
let meta = WildReportUI.label(metaText.isEmpty ? marker.title : metaText, font: .systemFont(ofSize: 12), color: AppColor.textSecondary, lines: 1)
textStack.addArrangedSubview(title)
textStack.addArrangedSubview(meta)
let badge = WildReportRiskStatusBadge(text: clue.statusText, color: marker.kind.color)
row.addArrangedSubview(iconWrap)
row.addArrangedSubview(textStack)
row.addArrangedSubview(badge)
textStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
badge.setContentHuggingPriority(.required, for: .horizontal)
return row
}
private func addInfoRows(for clue: WildReportRadarActiveClue, to stack: UIStackView) {
if let name = clue.name {
stack.addArrangedSubview(WildReportInfoRowView(title: clue.type == .green ? "摄影师" : "点位", value: name))
}
if let store = clue.storeName {
stack.addArrangedSubview(WildReportInfoRowView(title: "门店", value: store))
}
if let distance = clue.distanceText ?? clue.distance {
stack.addArrangedSubview(WildReportInfoRowView(title: "距离", value: distance))
}
if let updatedAt = clue.updatedAt {
stack.addArrangedSubview(WildReportInfoRowView(title: "时间", value: updatedAt))
}
if let reporter = clue.reporterName {
stack.addArrangedSubview(WildReportInfoRowView(title: "举报人", value: reporter))
}
if let phone = clue.maskedReporterPhone ?? clue.reporterPhone {
stack.addArrangedSubview(WildReportInfoRowView(title: "联系电话", value: phone))
}
}
private func addDescriptionBlocks(for clue: WildReportRadarActiveClue, to stack: UIStackView) {
if let summary = clue.summary {
stack.addArrangedSubview(makeBodyLabel(summary))
}
if let detail = clue.detail, detail != clue.summary {
stack.addArrangedSubview(makeBodyLabel(detail))
}
for content in clue.reportContents.prefix(3) {
let time = content.time.isEmpty ? "" : "\(content.time) "
stack.addArrangedSubview(makeBodyLabel("\(time)\(content.content)"))
}
}
private func addMediaRow(for clue: WildReportRadarActiveClue, to stack: UIStackView) {
guard !clue.images.isEmpty else { return }
let scroll = UIScrollView()
scroll.showsHorizontalScrollIndicator = false
let row = UIStackView()
row.axis = .horizontal
row.spacing = 10
scroll.addSubview(row)
row.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.height.equalTo(72)
}
for url in clue.images.prefix(9) {
let tile = WildReportRiskImageTile(url: url)
tile.onTap = { [weak self] in self?.presentImagePreview(url: url) }
row.addArrangedSubview(tile)
}
stack.addArrangedSubview(scroll)
scroll.snp.makeConstraints { make in
make.height.equalTo(72)
}
}
private func addProcessingBlock(for clue: WildReportRadarActiveClue, to stack: UIStackView) {
guard let record = clue.record else { return }
let box = UIView()
box.backgroundColor = UIColor(hex: 0xF8FAFF)
box.layer.cornerRadius = 12
box.layer.borderWidth = 1
box.layer.borderColor = AppColor.cardOutline.cgColor
let inner = UIStackView()
inner.axis = .vertical
inner.spacing = 8
box.addSubview(inner)
inner.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(12)
}
inner.addArrangedSubview(WildReportInfoRowView(title: "处理人", value: record.handlerName))
inner.addArrangedSubview(makeBodyLabel(record.processResult))
stack.addArrangedSubview(box)
}
private func makeButtonRow(for clue: WildReportRadarActiveClue) -> UIView {
let row = UIStackView()
row.axis = .horizontal
row.spacing = 10
row.distribution = .fillEqually
let navButton = WildReportUI.iconButton(
title: clue.type == .blue ? "刷新定位" : "导航到此",
imageName: clue.type == .blue ? "location.fill" : "map.fill",
style: .primary
)
navButton.addTarget(self, action: #selector(navigateTapped), for: .touchUpInside)
let shareButton = WildReportUI.iconButton(title: "分享", imageName: "square.and.arrow.up", style: .secondary)
shareButton.addTarget(self, action: #selector(shareTapped), for: .touchUpInside)
[navButton, shareButton].forEach { button in
button.snp.makeConstraints { make in make.height.equalTo(44) }
row.addArrangedSubview(button)
}
return row
}
private func makeLoadingView(text: String) -> UIView {
let card = WildReportCardView(spacing: 12, inset: 16)
card.stack.alignment = .center
loadingIndicator.startAnimating()
card.stack.addArrangedSubview(loadingIndicator)
card.stack.addArrangedSubview(WildReportUI.label(text, font: .systemFont(ofSize: 14), color: AppColor.textSecondary))
return card
}
private func makeInlineLoading(text: String) -> UIView {
let row = UIStackView()
row.axis = .horizontal
row.alignment = .center
row.spacing = 8
let indicator = UIActivityIndicatorView(style: .medium)
indicator.startAnimating()
row.addArrangedSubview(indicator)
row.addArrangedSubview(WildReportUI.label(text, font: .systemFont(ofSize: 13), color: AppColor.textSecondary))
return row
}
private func makeStateCard(icon: String, text: String) -> UIView {
let card = WildReportCardView(spacing: 12, inset: 18)
card.stack.alignment = .center
let image = UIImageView(image: UIImage(systemName: icon))
image.tintColor = AppColor.primary
image.contentMode = .scaleAspectFit
image.snp.makeConstraints { make in
make.size.equalTo(34)
}
let label = WildReportUI.label(text, font: .systemFont(ofSize: 15, weight: .semibold), color: AppColor.textSecondary, lines: 0)
label.textAlignment = .center
card.stack.addArrangedSubview(image)
card.stack.addArrangedSubview(label)
return card
}
private func makeHighlight(text: String, color: UIColor) -> UIView {
let label = WildReportUI.label(text, font: .systemFont(ofSize: 14, weight: .semibold), color: color, lines: 0)
label.backgroundColor = color.withAlphaComponent(0.08)
label.layer.cornerRadius = 10
label.clipsToBounds = true
label.textAlignment = .center
label.snp.makeConstraints { make in
make.height.greaterThanOrEqualTo(34)
}
return label
}
private func makeBodyLabel(_ text: String) -> UILabel {
WildReportUI.label(text, font: .systemFont(ofSize: 14), color: AppColor.textSecondary, lines: 0)
}
private func iconName(for kind: WildReportMapMarkerKind) -> String {
switch kind {
case .selfLocation: return "location.fill"
case .wildPhotographer: return "exclamationmark"
case .processingClue: return "clock.fill"
case .peerPhotographer: return "camera.fill"
}
}
private func presentImagePreview(url: String) {
present(WildReportRiskImagePreviewController(url: url), animated: true)
}
@objc private func navigateTapped() {
guard let marker = viewModel.selectedMarker,
case .activeClue(let clue) = marker.detail
else { return }
if clue.type == .blue {
Task {
await viewModel.refreshLocation(
api: api,
locationProvider: locationProvider,
scenicId: scenicIdProvider(),
scenicName: scenicNameProvider()
)
}
return
}
guard let target = viewModel.navigateToSelectedMarker() else { return }
let item = MKMapItem(placemark: MKPlacemark(coordinate: target.coordinate))
item.name = target.title
item.openInMaps(launchOptions: [
MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving
])
}
@objc private func shareTapped() {
viewModel.shareSelectedClue()
}
}
extension WildReportRiskMapViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? WildReportMapAnnotation else { return nil }
let identifier = "WildReportPulseMarkerAnnotationView"
let view = (mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? WildReportPulseMarkerAnnotationView)
?? WildReportPulseMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
view.annotation = annotation
view.apply(marker: annotation.marker, selected: annotation.marker.id == viewModel.selectedMarkerID)
return view
}
func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) {
guard let annotation = annotation as? WildReportMapAnnotation else { return }
Task {
await viewModel.selectMarker(
annotation.marker,
api: api,
scenicId: scenicIdProvider()
)
}
}
}
/// MKAnnotation
final class WildReportMapAnnotation: NSObject, MKAnnotation {
let marker: WildReportMapMarker
var coordinate: CLLocationCoordinate2D { marker.coordinate }
var title: String? { marker.title }
init(marker: WildReportMapMarker) {
self.marker = marker
super.init()
}
}
///
private final class WildReportPulseMarkerAnnotationView: MKAnnotationView {
private let pulseOuter = UIView()
private let pulseInner = UIView()
private let selectedRing = UIView()
private let dot = UIView()
private let label = UILabel()
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
frame = CGRect(x: 0, y: 0, width: 56, height: 56)
centerOffset = CGPoint(x: 0, y: -12)
canShowCallout = false
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(marker: WildReportMapMarker, selected: Bool) {
let color = marker.kind.color
pulseOuter.layer.borderColor = color.withAlphaComponent(0.18).cgColor
pulseInner.layer.borderColor = color.withAlphaComponent(0.28).cgColor
selectedRing.layer.borderColor = color.cgColor
selectedRing.isHidden = !selected
dot.backgroundColor = color
label.isHidden = marker.kind != .selfLocation
label.text = ""
}
private func setupUI() {
backgroundColor = .clear
[pulseOuter, pulseInner, selectedRing, dot, label].forEach(addSubview)
[pulseOuter, pulseInner, selectedRing].forEach { view in
view.backgroundColor = .clear
view.layer.borderWidth = 2
view.isUserInteractionEnabled = false
}
pulseOuter.layer.cornerRadius = 20
pulseInner.layer.cornerRadius = 14
selectedRing.layer.cornerRadius = 14
dot.layer.cornerRadius = 7
dot.layer.borderWidth = 2
dot.layer.borderColor = UIColor.white.cgColor
label.font = .systemFont(ofSize: 10, weight: .bold)
label.textColor = .white
label.textAlignment = .center
pulseOuter.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(40)
}
pulseInner.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(28)
}
selectedRing.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(28)
}
dot.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(14)
}
label.snp.makeConstraints { make in
make.center.equalTo(dot)
make.size.equalTo(14)
}
startPulse()
}
private func startPulse() {
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0.85
animation.toValue = 0.2
animation.duration = 1.2
animation.autoreverses = true
animation.repeatCount = .infinity
pulseOuter.layer.add(animation, forKey: "opacityPulse")
}
}
///
private final class WildReportRiskLegendView: UIView {
init(kind: WildReportMapMarkerKind) {
super.init(frame: .zero)
let row = UIStackView()
row.axis = .horizontal
row.alignment = .center
row.spacing = 5
let dot = UIView()
dot.backgroundColor = kind.color
dot.layer.cornerRadius = 4
dot.snp.makeConstraints { make in
make.size.equalTo(8)
}
let label = WildReportUI.label(kind.title, font: .systemFont(ofSize: 11, weight: .medium), color: AppColor.textSecondary, lines: 1)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.82
row.addArrangedSubview(dot)
row.addArrangedSubview(label)
addSubview(row)
row.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.greaterThanOrEqualToSuperview()
make.trailing.lessThanOrEqualToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
private final class WildReportRiskStatusBadge: UILabel {
init(text: String, color: UIColor) {
super.init(frame: .zero)
self.text = text
textColor = color
font = .systemFont(ofSize: 12, weight: .semibold)
textAlignment = .center
backgroundColor = color.withAlphaComponent(0.12)
layer.cornerRadius = 12
clipsToBounds = true
numberOfLines = 1
adjustsFontSizeToFitWidth = true
minimumScaleFactor = 0.8
snp.makeConstraints { make in
make.height.equalTo(24)
make.width.greaterThanOrEqualTo(64)
make.width.lessThanOrEqualTo(96)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
private final class WildReportRiskImageTile: UIView {
private let imageView = UIImageView()
var onTap: (() -> Void)?
init(url: String) {
super.init(frame: .zero)
backgroundColor = AppColor.pageBackgroundSoft
layer.cornerRadius = 10
layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
addSubview(imageView)
imageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
if let imageURL = URL(string: url) {
imageView.kf.setImage(with: imageURL, placeholder: UIImage(systemName: "photo.fill"))
} else {
imageView.image = UIImage(systemName: "photo.fill")
imageView.tintColor = AppColor.textTertiary
}
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapped)))
snp.makeConstraints { make in
make.size.equalTo(72)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapped() {
onTap?()
}
}
///
private final class WildReportRiskImagePreviewController: UIViewController {
private let url: String
private let imageView = UIImageView()
init(url: String) {
self.url = url
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .fullScreen
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
imageView.contentMode = .scaleAspectFit
view.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
if let imageURL = URL(string: url) {
imageView.kf.setImage(with: imageURL)
}
let closeButton = UIButton(type: .system)
closeButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
closeButton.tintColor = .white
closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
view.addSubview(closeButton)
closeButton.snp.makeConstraints { make in
make.top.trailing.equalTo(view.safeAreaLayoutGuide).inset(18)
make.size.equalTo(36)
}
}
@objc private func closeTapped() {
dismiss(animated: true)
}
}