Path: blob/a-new-beginning/Folium-iOS/Classes/Cells/TomatoCell.swift
2 views
//
// TomatoCell.swift
// Folium-iOS
//
// Created by Jarrod Norwell on 15/7/2025.
//
import UniformTypeIdentifiers
import UIKit
class TomatoCell : UICollectionViewCell {
var visualEffectView: UIVisualEffectView? = nil
var containerView: UIView? = nil
var imageView: UIImageView? = nil
var textLabel: UILabel? = nil
var optionsButton: UIButton? = nil
var hasCustomArtwork: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
if #available(iOS 26, *) {
visualEffectView = UIVisualEffectView(effect: UIGlassEffect(style: .regular))
guard let visualEffectView else {
return
}
visualEffectView.translatesAutoresizingMaskIntoConstraints = false
addSubview(visualEffectView)
visualEffectView.topAnchor.constraint(equalTo: topAnchor).isActive = true
visualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
visualEffectView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
visualEffectView.heightAnchor.constraint(equalTo: widthAnchor).isActive = true
visualEffectView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
} else {
containerView = UIView()
guard let containerView else {
return
}
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = .secondarySystemBackground
addSubview(containerView)
containerView.topAnchor.constraint(equalTo: topAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
containerView.heightAnchor.constraint(equalTo: widthAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
guard let viewForSubviews: UIView = visualEffectView?.contentView ?? containerView else {
return
}
imageView = .init()
guard let imageView else {
return
}
imageView.translatesAutoresizingMaskIntoConstraints = false
if #unavailable(iOS 26) {
imageView.backgroundColor = .tertiarySystemBackground
}
imageView.clipsToBounds = true
imageView.layer.cornerCurve = .continuous
addSubview(imageView)
let constant: CGFloat = if #available(iOS 26, *) { 8 } else { 4 }
imageView.topAnchor.constraint(equalTo: viewForSubviews.safeAreaLayoutGuide.topAnchor, constant: constant).isActive = true
imageView.leadingAnchor.constraint(equalTo: viewForSubviews.safeAreaLayoutGuide.leadingAnchor, constant: constant).isActive = true
imageView.bottomAnchor.constraint(equalTo: viewForSubviews.safeAreaLayoutGuide.bottomAnchor, constant: -constant).isActive = true
imageView.trailingAnchor.constraint(equalTo: viewForSubviews.safeAreaLayoutGuide.trailingAnchor, constant: -constant).isActive = true
textLabel = .init()
guard let textLabel else {
return
}
textLabel.translatesAutoresizingMaskIntoConstraints = false
textLabel.font = .bold(.body)
textLabel.textColor = .label
addSubview(textLabel)
textLabel.topAnchor.constraint(equalTo: viewForSubviews.safeAreaLayoutGuide.bottomAnchor, constant: 8).isActive = true
textLabel.leadingAnchor.constraint(equalTo: viewForSubviews.safeAreaLayoutGuide.leadingAnchor).isActive = true
textLabel.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
textLabel.trailingAnchor.constraint(lessThanOrEqualTo: viewForSubviews.safeAreaLayoutGuide.trailingAnchor).isActive = true
var configuration: UIButton.Configuration = if #available(iOS 26, *) {
.glass()
} else {
.filled()
}
if #unavailable(iOS 26) {
configuration.baseBackgroundColor = .secondarySystemBackground.withAlphaComponent(1 / 3)
configuration.baseForegroundColor = .label
}
configuration.buttonSize = .medium
configuration.cornerStyle = .capsule
configuration.image = .init(systemName: "ellipsis")?
.applyingSymbolConfiguration(.init(scale: .medium))
optionsButton = .init(configuration: configuration)
guard let optionsButton else {
return
}
optionsButton.translatesAutoresizingMaskIntoConstraints = false
optionsButton.showsMenuAsPrimaryAction = true
if #unavailable(iOS 26) {
optionsButton.layer.shadowColor = UIColor.black.cgColor
optionsButton.layer.shadowOpacity = 1 / 4
optionsButton.layer.shadowRadius = 20
optionsButton.layer.shadowOffset = .init(width: 0, height: 10)
}
addSubview(optionsButton)
if #available(iOS 26, *) {
optionsButton.centerXAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor,
constant: -10.0 + (constant / 2)).isActive = true
optionsButton.centerYAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.topAnchor,
constant: 10.0 + (constant / 2)).isActive = true
} else {
optionsButton.topAnchor.constraint(equalTo: topAnchor, constant: 20).isActive = true
optionsButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20).isActive = true
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
guard let viewForSubview: UIView = visualEffectView ?? containerView, let imageView, let optionsButton else {
return
}
if #available(iOS 26, *) {
applyCutout(from: optionsButton, to: imageView, expandBy: 8)
viewForSubview.cornerConfiguration = .corners(radius: .fixed(optionsButton.frame.height + 10.0))
imageView.layer.cornerRadius = optionsButton.frame.height + 10.0 - 8.0
} else {
viewForSubview.layer.cornerRadius = optionsButton.frame.height / 2.0 + 20.0
imageView.layer.cornerRadius = viewForSubview.layer.cornerRadius - 4.0
}
}
var game: NewTomatoGame? = nil
func set(tomatoGame: NewTomatoGame, controller: UIViewController) {
game = tomatoGame
guard let imageView, let textLabel, let optionsButton else {
return
}
guard let documentDirectoryURL: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return
}
let artworkDirectoryURL: URL = documentDirectoryURL.appending(component: tomatoGame.core.string).appending(component: "artworks")
let imageURL: URL = artworkDirectoryURL.appending(component: "\(tomatoGame.details.name.lowercased()).png")
let fileExists = FileManager.default.fileExists(atPath: imageURL.path)
hasCustomArtwork = fileExists
imageView.image = if fileExists {
.init(contentsOfFile: imageURL.path)
} else {
nil
}
textLabel.text = tomatoGame.details.name
optionsButton.menu = .init(children: [
UIMenu(title: .localized(for: .artwork), image: .init(systemName: "photo"), children: [
UIDeferredMenuElement.uncached { completion in
var elements: [UIMenuElement] = [
UIAction(title: .localized(for: .import), image: .init(systemName: "arrow.down.circle")) { _ in
let imagePickerController: UIImagePickerController = .init()
imagePickerController.allowsEditing = true
imagePickerController.delegate = self
imagePickerController.mediaTypes = [UTType.image.identifier]
imagePickerController.modalPresentationStyle = .fullScreen
controller.present(imagePickerController, animated: true)
}
]
if fileExists {
elements.append(UIAction(title: .localized(for: .delete),
image: .init(systemName: "minus.circle"),
attributes: .destructive) { _ in
Task {
try FileManager.default.removeItem(at: imageURL)
self.hasCustomArtwork = false
guard let controller = controller as? LibraryController else {
return
}
await controller.populateLibrary()
}
})
}
completion(elements)
}
]),
UIAction(title: .localized(for: .deleteGame), image: .init(systemName: "minus.circle"), attributes: .destructive, handler: { _ in
let alertController: UIAlertController = .init(title: "Delete \"\(tomatoGame.details.name)\"?",
message: .localized(for: .deleteGameSecondaryText),
preferredStyle: .alert)
alertController.addAction(.init(title: .localized(for: .dismiss), style: .cancel))
alertController.addAction(.init(title: .localized(for: .delete), style: .destructive, handler: { _ in
let task = Task {
try FileManager.default.removeItem(at: tomatoGame.details.url)
guard let controller = controller as? LibraryController else {
return
}
await controller.populateLibrary()
}
Task {
switch await task.result {
case .success:
break
case .failure(let error):
print(#function, #line, error.localizedDescription)
}
}
}))
controller.present(alertController, animated: true)
})
])
}
}
extension TomatoCell : UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let image: UIImage = info[.editedImage] as? UIImage else {
return
}
guard let imageView, let game else {
return
}
guard let documentDirectoryURL: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return
}
let artworkDirectoryURL: URL = documentDirectoryURL.appending(component: game.core.string).appending(component: "artworks")
let url: URL = artworkDirectoryURL.appending(component: "\(game.details.name.lowercased()).png")
Task {
if FileManager.default.fileExists(atPath: url.path) {
try FileManager.default.removeItem(at: url)
}
if let data = image.pngData() {
try data.write(to: url)
}
let fileExists = FileManager.default.fileExists(atPath: url.path)
hasCustomArtwork = fileExists
if fileExists {
imageView.image = .init(contentsOfFile: url.path)
}
picker.dismiss(animated: true)
}
}
}