Path: blob/a-new-beginning/Folium-iOS/Controllers/EmulationControllers/CherryController.swift
2 views
//
// CherryController.swift
// Folium
//
// Created by Jarrod Norwell on 20/1/2026.
//
import Cherry
import GameController
import UIKit
class CherryController : UIViewController {
var imageView: UIImageView? = nil,
secondaryImageView: UIImageView? = nil
var visualEffectView: UIVisualEffectView? = nil,
glassVisualEffectView: UIVisualEffectView? = nil
var settingsButton: UIButton? = nil,
selectButton: UIButton? = nil,
startButton: UIButton? = nil
var asterixButton: UIButton? = nil,
zeroButton: UIButton? = nil,
hashtagButton: UIButton? = nil,
sevenButton: UIButton? = nil,
eightButton: UIButton? = nil,
nineButton: UIButton? = nil,
fourButton: UIButton? = nil,
fiveButton: UIButton? = nil,
sixButton: UIButton? = nil,
oneButton: UIButton? = nil,
twoButton: UIButton? = nil,
threeButton: UIButton? = nil
var thumbstick: LatestControllerThumbstick? = nil
var portraitBottomConstraint: NSLayoutConstraint? = nil
var landscapeLeftConstraint: NSLayoutConstraint? = nil,
landscapeRightConstraint: NSLayoutConstraint? = nil
var constraints: (portrait: [NSLayoutConstraint], landscape: [NSLayoutConstraint]) = ([], [])
var game: NewCherryGame
var cherry: Cherry
init(_ game: NewCherryGame, _ cherry: Cherry) {
self.game = game
self.cherry = cherry
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
secondaryImageView = UIImageView()
guard let secondaryImageView else {
return
}
secondaryImageView.translatesAutoresizingMaskIntoConstraints = false
secondaryImageView.backgroundColor = .label
view.addSubview(secondaryImageView)
visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial))
guard let visualEffectView else {
return
}
visualEffectView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(visualEffectView)
if #available(iOS 26, *) {
let effect: UIGlassContainerEffect = UIGlassContainerEffect()
effect.spacing = 20
glassVisualEffectView = UIVisualEffectView(effect: effect)
guard let glassVisualEffectView else {
return
}
glassVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
visualEffectView.contentView.addSubview(glassVisualEffectView)
}
let viewToAddSubviews: UIView = glassVisualEffectView?.contentView ?? visualEffectView.contentView
imageView = UIImageView()
guard let imageView else {
return
}
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .label
if #available(iOS 26, *) {
imageView.clipsToBounds = true
imageView.cornerConfiguration = .corners(radius: .fixed(15.0))
} else {
imageView.clipsToBounds = true
imageView.layer.cornerCurve = .continuous
imageView.layer.cornerRadius = 15.0
}
viewToAddSubviews.addSubview(imageView)
let settingsConfiguration: UIButton.Configuration = .configuration(.medium, .capsule, UIImage(systemName: "ellipsis"), .medium)
settingsButton = .button(with: settingsConfiguration,
actions: ({ _ in }, { _ in }), UIMenu(children: [
UIMenu(options: .displayInline, children: [
UIDeferredMenuElement.uncached { completion in
Task {
completion([
UIAction(title: "Stop & Exit", image: UIImage(systemName: "stop"), attributes: .destructive) { _ in
let alertController: UIAlertController = UIAlertController(title: "Stop & Exit",
message: "Are you sure you want to stop & exit? Unsaved progress will be lost",
preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: .localized(for: .dismiss), style: .cancel))
alertController.addAction(UIAlertAction(title: "Stop & Exit", style: .destructive, handler: { _ in
Task {
await self.cherry.stop()
self.dismiss(animated: true)
}
}))
self.present(alertController, animated: true)
},
UIAction(title: await self.cherry.isPaused ? "Resume" : "Pause",
image: await self.cherry.isPaused ? UIImage(systemName: "play") : UIImage(systemName: "pause")) { _ in
Task {
await self.cherry.pause(await !self.cherry.isPaused)
}
}
])
}
}
]),
UIMenu(options: .displayInline, children: [
UIMenu(title: "Delete State", image: UIImage(systemName: "minus.circle"), options: .destructive, preferredElementSize: .small, children: [
UIDeferredMenuElement.uncached { completion in
guard let documentDirectory: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
completion([])
return
}
let statesDirectory: URL = documentDirectory.appending(component: "Cherry").appending(component: "states")
let stateNumbers: [Int] = [0, 1, 2]
let elements: [UIAction] = stateNumbers.map { state in
let url: URL = statesDirectory.appending(component: "\(self.game.details.name)_\(state).state")
let fileExists: Bool = FileManager.default.fileExists(atPath: url.path)
return .init(image: UIImage(systemName: "\(state + 1).circle"), attributes: fileExists ? .destructive : .disabled) { _ in
Task {
try FileManager.default.removeItem(at: url)
}
}
}
completion(elements)
}
]),
UIMenu(title: "Save State", image: .init(systemName: "arrow.down.circle"), preferredElementSize: .small, children: [
UIDeferredMenuElement.uncached { completion in
guard let documentDirectory: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
completion([])
return
}
let statesDirectory: URL = documentDirectory.appending(component: "Cherry").appending(component: "states")
let stateNumbers: [Int] = [0, 1, 2]
let elements: [UIAction] = stateNumbers.map { state in
let url: URL = statesDirectory.appending(component: "\(self.game.details.name)_\(state).state")
let fileExists: Bool = FileManager.default.fileExists(atPath: url.path)
return .init(image: UIImage(systemName: "\(state + 1).circle")) { _ in
if fileExists {
let alertController: UIAlertController = UIAlertController(title: "Overwrite?",
message: "Are you sure you want to overwrite? This action is irreversible",
preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: .localized(for: .dismiss), style: .cancel))
alertController.addAction(UIAlertAction(title: "Overwrite", style: .destructive, handler: { _ in
Task {
// await self.cherry.save(state: url)
}
}))
self.present(alertController, animated: true)
} else {
Task {
// await self.cherry.save(state: url)
}
}
}
}
completion(elements)
}
]),
UIMenu(title: "Load State", image: .init(systemName: "arrow.up.circle"), preferredElementSize: .small, children: [
UIDeferredMenuElement.uncached { completion in
guard let documentDirectory: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
completion([])
return
}
let statesDirectory: URL = documentDirectory.appending(component: "Cherry").appending(component: "states")
let stateNumbers: [Int] = [0, 1, 2]
let elements: [UIAction] = stateNumbers.map { state in
let url: URL = statesDirectory.appending(component: "\(self.game.details.name)_\(state).state")
let fileExists: Bool = FileManager.default.fileExists(atPath: url.path)
return .init(image: UIImage(systemName: "\(state + 1).circle"), attributes: fileExists ? [] : .disabled) { _ in
Task {
// await self.cherry.load(state: url)
}
}
}
completion(elements)
}
]),
UIMenu(options: .displayInline, children: [
UIAction(title: "Capture Still Image", image: UIImage(systemName: "photo.badge.arrow.down")) { _ in
Task {
await self.cherry.pause(true)
guard let imageView: UIImageView = self.imageView, let image: UIImage = imageView.image else {
return
}
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
await self.cherry.pause(false)
}
}
])
])
]))
guard let settingsButton else {
return
}
viewToAddSubviews.addSubview(settingsButton)
let selectConfiguration: UIButton.Configuration = .configuration(.medium, .capsule, UIImage(systemName: "minus"), .medium)
selectButton = .button(with: selectConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_left_button, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_left_button, player: 0, pressed: false)
}))
guard let selectButton else {
return
}
viewToAddSubviews.addSubview(selectButton)
let startConfiguration: UIButton.Configuration = .configuration(.medium, .capsule, UIImage(systemName: "plus"), .medium)
startButton = .button(with: startConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_right_button, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_right_button, player: 0, pressed: false)
}))
guard let startButton else {
return
}
viewToAddSubviews.addSubview(startButton)
// TODO: buttons
let asterixConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "circle"), .large)
asterixButton = .button(with: asterixConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_asterisk, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_asterisk, player: 0, pressed: false)
}))
guard let asterixButton else {
return
}
viewToAddSubviews.addSubview(asterixButton)
let zeroConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "0.circle.fill"), .large)
zeroButton = .button(with: zeroConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_0, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_0, player: 0, pressed: false)
}))
guard let zeroButton else {
return
}
viewToAddSubviews.addSubview(zeroButton)
let hashtagConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "number"), .large)
hashtagButton = .button(with: hashtagConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_hash, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_hash, player: 0, pressed: false)
}))
guard let hashtagButton else {
return
}
viewToAddSubviews.addSubview(hashtagButton)
let sevenConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "7.circle.fill"), .large)
sevenButton = .button(with: sevenConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_7, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_7, player: 0, pressed: false)
}))
guard let sevenButton else {
return
}
viewToAddSubviews.addSubview(sevenButton)
let eightConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "8.circle.fill"), .large)
eightButton = .button(with: eightConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_8, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_8, player: 0, pressed: false)
}))
guard let eightButton else {
return
}
viewToAddSubviews.addSubview(eightButton)
let nineConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "9.circle.fill"), .large)
nineButton = .button(with: nineConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_9, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_9, player: 0, pressed: false)
}))
guard let nineButton else {
return
}
viewToAddSubviews.addSubview(nineButton)
let fourConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "4.circle.fill"), .large)
fourButton = .button(with: fourConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_4, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_4, player: 0, pressed: false)
}))
guard let fourButton else {
return
}
viewToAddSubviews.addSubview(fourButton)
let fiveConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "5.circle.fill"), .large)
fiveButton = .button(with: fiveConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_5, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_5, player: 0, pressed: false)
}))
guard let fiveButton else {
return
}
viewToAddSubviews.addSubview(fiveButton)
let sixConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "6.circle.fill"), .large)
sixButton = .button(with: sixConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_6, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_6, player: 0, pressed: false)
}))
guard let sixButton else {
return
}
viewToAddSubviews.addSubview(sixButton)
let oneConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "1.circle.fill"), .large)
oneButton = .button(with: oneConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_1, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_1, player: 0, pressed: false)
}))
guard let oneButton else {
return
}
viewToAddSubviews.addSubview(oneButton)
let twoConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "2.circle.fill"), .large)
twoButton = .button(with: twoConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_2, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_2, player: 0, pressed: false)
}))
guard let twoButton else {
return
}
viewToAddSubviews.addSubview(twoButton)
let threeConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "3.circle.fill"), .large)
threeButton = .button(with: threeConfiguration,
actions: ({ _ in
await self.cherry.button(button: .k_3, player: 0, pressed: true)
}, { _ in
await self.cherry.button(button: .k_3, player: 0, pressed: false)
}))
guard let threeButton else {
return
}
viewToAddSubviews.addSubview(threeButton)
// TODO: buttons
thumbstick = LatestControllerThumbstick(.left, self, 1 / 2)
guard let thumbstick else {
return
}
thumbstick.translatesAutoresizingMaskIntoConstraints = false
viewToAddSubviews.addSubview(thumbstick)
if iPhone {
let padding: CGFloat = 50 * 3
let divider: CGFloat = 2
portraitBottomConstraint = secondaryImageView.bottomAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.bottomAnchor)
guard let portraitBottomConstraint else {
return
}
constraints.portrait.append(contentsOf: [
secondaryImageView.topAnchor.constraint(equalTo: view.topAnchor),
secondaryImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
secondaryImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
portraitBottomConstraint,
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
imageView.topAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.topAnchor,
constant: 20.0),
imageView.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
imageView.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20.0),
imageView.heightAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.widthAnchor,
multiplier: 3.0 / 4.0),
settingsButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
settingsButton.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
selectButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
selectButton.trailingAnchor.constraint(equalTo: settingsButton.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
startButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
startButton.leadingAnchor.constraint(equalTo: settingsButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
zeroButton.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
zeroButton.bottomAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
zeroButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
zeroButton.heightAnchor.constraint(equalTo: zeroButton.safeAreaLayoutGuide.widthAnchor),
asterixButton.trailingAnchor.constraint(equalTo: zeroButton.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
asterixButton.bottomAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
asterixButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
asterixButton.heightAnchor.constraint(equalTo: zeroButton.safeAreaLayoutGuide.widthAnchor),
hashtagButton.leadingAnchor.constraint(equalTo: zeroButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
hashtagButton.bottomAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
hashtagButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
hashtagButton.heightAnchor.constraint(equalTo: zeroButton.safeAreaLayoutGuide.widthAnchor),
eightButton.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
eightButton.bottomAnchor.constraint(equalTo: zeroButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
eightButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
eightButton.heightAnchor.constraint(equalTo: eightButton.safeAreaLayoutGuide.widthAnchor),
sevenButton.trailingAnchor.constraint(equalTo: eightButton.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
sevenButton.bottomAnchor.constraint(equalTo: asterixButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
sevenButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
sevenButton.heightAnchor.constraint(equalTo: eightButton.safeAreaLayoutGuide.widthAnchor),
nineButton.leadingAnchor.constraint(equalTo: eightButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
nineButton.bottomAnchor.constraint(equalTo: hashtagButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
nineButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
nineButton.heightAnchor.constraint(equalTo: eightButton.safeAreaLayoutGuide.widthAnchor),
fiveButton.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
fiveButton.bottomAnchor.constraint(equalTo: eightButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
fiveButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
fiveButton.heightAnchor.constraint(equalTo: fiveButton.safeAreaLayoutGuide.widthAnchor),
fourButton.trailingAnchor.constraint(equalTo: fiveButton.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
fourButton.bottomAnchor.constraint(equalTo: sevenButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
fourButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
fourButton.heightAnchor.constraint(equalTo: fiveButton.safeAreaLayoutGuide.widthAnchor),
sixButton.leadingAnchor.constraint(equalTo: fiveButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
sixButton.bottomAnchor.constraint(equalTo: nineButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
sixButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
sixButton.heightAnchor.constraint(equalTo: fiveButton.safeAreaLayoutGuide.widthAnchor),
twoButton.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
twoButton.bottomAnchor.constraint(equalTo: fiveButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
twoButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
twoButton.heightAnchor.constraint(equalTo: twoButton.safeAreaLayoutGuide.widthAnchor),
oneButton.trailingAnchor.constraint(equalTo: twoButton.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
oneButton.bottomAnchor.constraint(equalTo: fourButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
oneButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
oneButton.heightAnchor.constraint(equalTo: twoButton.safeAreaLayoutGuide.widthAnchor),
threeButton.leadingAnchor.constraint(equalTo: twoButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
threeButton.bottomAnchor.constraint(equalTo: sixButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
threeButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
threeButton.heightAnchor.constraint(equalTo: twoButton.safeAreaLayoutGuide.widthAnchor),
thumbstick.topAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.bottomAnchor,
constant: 20.0),
thumbstick.bottomAnchor.constraint(equalTo: twoButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
thumbstick.widthAnchor.constraint(equalTo: thumbstick.safeAreaLayoutGuide.heightAnchor),
thumbstick.centerXAnchor.constraint(equalTo: viewToAddSubviews.safeAreaLayoutGuide.centerXAnchor)
])
// 40 • 20 • 20 • 40 = 120
if #available(iOS 26, *), let glassVisualEffectView {
constraints.portrait.append(contentsOf: [
glassVisualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
glassVisualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
glassVisualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
glassVisualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
landscapeLeftConstraint = secondaryImageView.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.leadingAnchor)
landscapeRightConstraint = secondaryImageView.trailingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor)
guard let landscapeLeftConstraint, let landscapeRightConstraint else {
return
}
constraints.landscape.append(contentsOf: [
secondaryImageView.topAnchor.constraint(equalTo: view.topAnchor),
secondaryImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
landscapeLeftConstraint,
landscapeRightConstraint,
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
startButton.topAnchor.constraint(equalTo: settingsButton.safeAreaLayoutGuide.bottomAnchor,
constant: 20.0),
startButton.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
selectButton.bottomAnchor.constraint(equalTo: settingsButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
selectButton.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
settingsButton.centerYAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerYAnchor),
settingsButton.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
imageView.topAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.topAnchor,
constant: 20.0),
imageView.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
imageView.bottomAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.bottomAnchor,
constant: -20.0),
imageView.widthAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.heightAnchor,
multiplier: 4.0 / 3.0)
])
if #available(iOS 26, *), let glassVisualEffectView {
constraints.landscape.append(contentsOf: [
glassVisualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
glassVisualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
glassVisualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
glassVisualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
} else {
let padding: CGFloat = 180 * 3
let divider: CGFloat = 2
portraitBottomConstraint = secondaryImageView.bottomAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.bottomAnchor)
guard let portraitBottomConstraint else {
return
}
constraints.portrait.append(contentsOf: [
secondaryImageView.topAnchor.constraint(equalTo: view.topAnchor),
secondaryImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
secondaryImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
portraitBottomConstraint,
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
imageView.topAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.topAnchor,
constant: 20.0),
imageView.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
imageView.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20.0),
imageView.heightAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.widthAnchor,
multiplier: 3.0 / 4.0),
settingsButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
settingsButton.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
selectButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
selectButton.trailingAnchor.constraint(equalTo: settingsButton.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
startButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
startButton.leadingAnchor.constraint(equalTo: settingsButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
zeroButton.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
zeroButton.bottomAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
zeroButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
zeroButton.heightAnchor.constraint(equalTo: zeroButton.safeAreaLayoutGuide.widthAnchor),
asterixButton.trailingAnchor.constraint(equalTo: zeroButton.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
asterixButton.bottomAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
asterixButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
asterixButton.heightAnchor.constraint(equalTo: zeroButton.safeAreaLayoutGuide.widthAnchor),
hashtagButton.leadingAnchor.constraint(equalTo: zeroButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
hashtagButton.bottomAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
hashtagButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
hashtagButton.heightAnchor.constraint(equalTo: zeroButton.safeAreaLayoutGuide.widthAnchor),
eightButton.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
eightButton.bottomAnchor.constraint(equalTo: zeroButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
eightButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
eightButton.heightAnchor.constraint(equalTo: eightButton.safeAreaLayoutGuide.widthAnchor),
sevenButton.trailingAnchor.constraint(equalTo: eightButton.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
sevenButton.bottomAnchor.constraint(equalTo: asterixButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
sevenButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
sevenButton.heightAnchor.constraint(equalTo: eightButton.safeAreaLayoutGuide.widthAnchor),
nineButton.leadingAnchor.constraint(equalTo: eightButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
nineButton.bottomAnchor.constraint(equalTo: hashtagButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
nineButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
nineButton.heightAnchor.constraint(equalTo: eightButton.safeAreaLayoutGuide.widthAnchor),
fiveButton.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
fiveButton.bottomAnchor.constraint(equalTo: eightButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
fiveButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
fiveButton.heightAnchor.constraint(equalTo: fiveButton.safeAreaLayoutGuide.widthAnchor),
fourButton.trailingAnchor.constraint(equalTo: fiveButton.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
fourButton.bottomAnchor.constraint(equalTo: sevenButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
fourButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
fourButton.heightAnchor.constraint(equalTo: fiveButton.safeAreaLayoutGuide.widthAnchor),
sixButton.leadingAnchor.constraint(equalTo: fiveButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
sixButton.bottomAnchor.constraint(equalTo: nineButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
sixButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
sixButton.heightAnchor.constraint(equalTo: fiveButton.safeAreaLayoutGuide.widthAnchor),
twoButton.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
twoButton.bottomAnchor.constraint(equalTo: fiveButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
twoButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
twoButton.heightAnchor.constraint(equalTo: twoButton.safeAreaLayoutGuide.widthAnchor),
oneButton.trailingAnchor.constraint(equalTo: twoButton.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
oneButton.bottomAnchor.constraint(equalTo: fourButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
oneButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
oneButton.heightAnchor.constraint(equalTo: twoButton.safeAreaLayoutGuide.widthAnchor),
threeButton.leadingAnchor.constraint(equalTo: twoButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
threeButton.bottomAnchor.constraint(equalTo: sixButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
threeButton.widthAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.widthAnchor, multiplier: 1 / 3, constant: -(padding / divider)),
threeButton.heightAnchor.constraint(equalTo: twoButton.safeAreaLayoutGuide.widthAnchor),
thumbstick.topAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.bottomAnchor,
constant: 20.0),
thumbstick.bottomAnchor.constraint(equalTo: twoButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
thumbstick.widthAnchor.constraint(equalTo: thumbstick.safeAreaLayoutGuide.heightAnchor),
thumbstick.centerXAnchor.constraint(equalTo: viewToAddSubviews.safeAreaLayoutGuide.centerXAnchor)
])
if #available(iOS 26, *), let glassVisualEffectView {
constraints.portrait.append(contentsOf: [
glassVisualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
glassVisualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
glassVisualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
glassVisualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
landscapeLeftConstraint = secondaryImageView.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.leadingAnchor)
landscapeRightConstraint = secondaryImageView.trailingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor)
guard let landscapeLeftConstraint, let landscapeRightConstraint else {
return
}
constraints.landscape.append(contentsOf: [
secondaryImageView.topAnchor.constraint(equalTo: view.topAnchor),
secondaryImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
landscapeLeftConstraint,
landscapeRightConstraint,
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
startButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
startButton.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor,
constant: 20),
selectButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
selectButton.trailingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
settingsButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
settingsButton.leadingAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
imageView.topAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.topAnchor,
constant: 20.0),
imageView.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
imageView.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
imageView.widthAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.heightAnchor,
multiplier: 4.0 / 3.0)
])
}
switch interfaceOrientation() {
case .portrait:
view.addConstraints(constraints.portrait)
case .landscapeLeft, .landscapeRight:
view.addConstraints(constraints.landscape)
default:
break
}
Task {
_ = await cherry.insert(game.details.url)
await cherry.framebuffer { framebuffer in
guard let cgImage = CGImage.genericRGB888(framebuffer, 256, 192) else {
return
}
imageView.image = .init(cgImage: cgImage)
secondaryImageView.image = .init(cgImage: cgImage)
}
await cherry.start()
}
Task {
await GCController.startWirelessControllerDiscovery()
}
NotificationCenter.default.addObserver(forName: .applicationStateDidChange, object: nil, queue: .current) { notification in
guard let applicationState: ApplicationState = notification.object as? ApplicationState else {
return
}
Task {
await self.cherry.pause(applicationState.shouldPause)
}
}
#if !targetEnvironment(simulator)
NotificationCenter.default.addObserver(forName: .GCControllerDidConnect, object: nil, queue: .current) { notification in
guard let controller: GCController = notification.object as? GCController,
let _: GCExtendedGamepad = controller.extendedGamepad else {
return
}
visualEffectView.contentView.subviews.filter { subview in
subview.isKind(of: UIButton.classForCoder()) && subview != settingsButton
}.forEach { button in
UIView.animate(withDuration: 1 / 3) {
button.alpha = 1 / 3
}
}
/*
extendedGamepad.buttonA.pressedChangedHandler = { element, value, pressed in
Task {
await self.cherry.button(button: .a, player: controller.playerIndex.rawValue, pressed: pressed)
}
}
extendedGamepad.buttonB.pressedChangedHandler = { element, value, pressed in
Task {
await self.cherry.button(button: .b, player: controller.playerIndex.rawValue, pressed: pressed)
}
}
extendedGamepad.dpad.up.pressedChangedHandler = { element, value, pressed in
Task {
await self.cherry.button(button: .up, player: controller.playerIndex.rawValue, pressed: pressed)
}
}
extendedGamepad.dpad.down.pressedChangedHandler = { element, value, pressed in
Task {
await self.cherry.button(button: .down, player: controller.playerIndex.rawValue, pressed: pressed)
}
}
extendedGamepad.dpad.left.pressedChangedHandler = { element, value, pressed in
Task {
await self.cherry.button(button: .left, player: controller.playerIndex.rawValue, pressed: pressed)
}
}
extendedGamepad.dpad.right.pressedChangedHandler = { element, value, pressed in
Task {
await self.cherry.button(button: .right, player: controller.playerIndex.rawValue, pressed: pressed)
}
}
extendedGamepad.leftShoulder.pressedChangedHandler = { element, value, pressed in
Task {
await self.cherry.button(button: .l, player: controller.playerIndex.rawValue, pressed: pressed)
}
}
extendedGamepad.rightShoulder.pressedChangedHandler = { element, value, pressed in
Task {
await self.cherry.button(button: .r, player: controller.playerIndex.rawValue, pressed: pressed)
}
}
if let buttonOptions = extendedGamepad.buttonOptions {
buttonOptions.pressedChangedHandler = { element, value, pressed in
Task {
await self.cherry.button(button: .select, player: controller.playerIndex.rawValue, pressed: pressed)
}
}
}
extendedGamepad.buttonMenu.pressedChangedHandler = { element, value, pressed in
Task {
await self.cherry.button(button: .start, player: controller.playerIndex.rawValue, pressed: pressed)
}
}
*/
}
NotificationCenter.default.addObserver(forName: .GCControllerDidDisconnect, object: nil, queue: .current) { notification in
visualEffectView.contentView.subviews.filter { subview in
subview.isKind(of: UIButton.classForCoder()) && subview != settingsButton
}.forEach { button in
UIView.animate(withDuration: 1 / 3) {
button.alpha = 1
}
}
}
#endif
}
override var prefersHomeIndicatorAutoHidden: Bool { true }
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .portrait }
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
switch interfaceOrientation() {
case .portrait:
guard let portraitBottomConstraint else {
return
}
portraitBottomConstraint.constant = view.safeAreaInsets.top
case .landscapeLeft:
guard let landscapeLeftConstraint, let landscapeRightConstraint else {
return
}
landscapeLeftConstraint.constant = -view.safeAreaInsets.right
landscapeRightConstraint.constant = view.safeAreaInsets.right
case .landscapeRight:
guard let landscapeLeftConstraint, let landscapeRightConstraint else {
return
}
landscapeLeftConstraint.constant = -view.safeAreaInsets.left
landscapeRightConstraint.constant = view.safeAreaInsets.left
default:
break
}
Task {
view.setNeedsUpdateConstraints()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
GCController.stopWirelessControllerDiscovery()
NotificationCenter.default.removeObserver(self)
}
override func viewWillTransition(to size: CGSize, with coordinator: any UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { context in } completion: { context in
switch self.interfaceOrientation() {
case .portrait:
self.view.removeConstraints(self.constraints.landscape)
self.view.addConstraints(self.constraints.portrait)
case .landscapeLeft, .landscapeRight:
self.view.removeConstraints(self.constraints.portrait)
self.view.addConstraints(self.constraints.landscape)
default:
break
}
self.view.setNeedsUpdateConstraints()
}
}
}
extension CherryController {
@objc func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
if #available(iOS 17.5, *), let settingsButton {
UINotificationFeedbackGenerator(view: settingsButton).notificationOccurred(error == nil ? .success : .error)
} else {
UINotificationFeedbackGenerator().notificationOccurred(error == nil ? .success : .error)
}
}
}
extension CherryController : LatestControllerThumbstickDelegate {
func touchBegan(with thumbstick: LatestThumbstick, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex) {}
func touchEnded(with thumbstick: LatestThumbstick, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex) {}
func touchMoved(with thumbstick: LatestThumbstick, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex) {}
func touchBegan(with thumbstick: LatestThumbstick, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex) async {
}
func touchEnded(with thumbstick: LatestThumbstick, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex) async {
await cherry.button(button: .k_up, player: 0, pressed: false)
await cherry.button(button: .k_down, player: 0, pressed: false)
await cherry.button(button: .k_left, player: 0, pressed: false)
await cherry.button(button: .k_right, player: 0, pressed: false)
}
func touchMoved(with thumbstick: LatestThumbstick, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex) async {
let deadZone: Float = 0.2
let x = position.x
let y = position.y
// LEFT / RIGHT
if x <= -deadZone {
await cherry.button(button: .k_left, player: 0, pressed: true)
} else {
await cherry.button(button: .k_left, player: 0, pressed: false)
}
if x >= deadZone {
await cherry.button(button: .k_right, player: 0, pressed: true)
} else {
await cherry.button(button: .k_right, player: 0, pressed: false)
}
// UP / DOWN
if y >= deadZone {
await cherry.button(button: .k_up, player: 0, pressed: true)
} else {
await cherry.button(button: .k_up, player: 0, pressed: false)
}
if y <= -deadZone {
await cherry.button(button: .k_down, player: 0, pressed: true)
} else {
await cherry.button(button: .k_down, player: 0, pressed: false)
}
}
}