Add Snake Game Inside PDF in Swift for iOS
Play a quick game of old school snake. :)
//// Copyright © 2019-2025 PSPDFKit GmbH. All rights reserved.//// The Nutrient sample applications are licensed with a modified BSD license.// Please see License for details. This notice may not be removed from this file.//
import PSPDFKitimport PSPDFKitUI
class PSPDFSnakeExample: Example {
override init() { super.init() title = "Snake Game" contentDescription = "Play a quick game of old school snake" category = .annotations priority = 500 }
// MARK: Create document and viewcontroller override func invoke(with delegate: ExampleRunnerDelegate) -> UIViewController? { let document = createDocument() let controller = SnakeViewController(document: document) { // Need to show the whole document $0.userInterfaceViewMode = .always } return controller }
func createDocument() -> Document { // Set up configuration to create a new document. let configuration = Processor.Configuration()
// Add an empty page let emptyPageTemplate = PageTemplate.blank let newPageConfiguration = PDFNewPageConfiguration(pageTemplate: emptyPageTemplate) { $0.pageMargins = UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50) $0.pageRotation = .rotation90 } configuration.addNewPage(at: 0, configuration: newPageConfiguration)
let outputFileURL = FileHelper.temporaryPDFFileURL(prefix: "new-document") do { // Invoke processor to create new document. let processor = Processor(configuration: configuration, securityOptions: nil) processor.delegate = self as? ProcessorDelegate try processor.write(toFileURL: outputFileURL) } catch { print("Error while processing document: \(error)") }
let newDocument = Document(url: outputFileURL) newDocument.title = "Snake!" return newDocument }}
private class SnakeViewController: PDFViewController {
// MARK: Properties let increment = 20 var xEnd = 70 var yEnd = 210 var score = 0
enum Direction { case up case down case left case right case none } var currentDirection = Direction.none var previousDirection = Direction.none
// Used to indicate the current state of the game enum GameState { case playing case paused } var currentGameState = GameState.paused
var timer = Timer()
// We use these properties to get our snake and apple since this way it's easier to work on the same object in various methods var snake: PolyLineAnnotation { return (self.document?.annotationsForPage(at: 0, type: .polyLine) as! [PolyLineAnnotation]).first! } var apple: LineAnnotation { return (self.document?.annotationsForPage(at: 0, type: .line) as! [LineAnnotation]).first! }
// MARK: Initialization and key commands override func commonInit(with document: Document?, configuration: PDFConfiguration) { super.commonInit(with: document, configuration: configuration) // Create the snake and the apple createSnake() createApple() // Add the scoreboard let scoreButton = UIBarButtonItem(title: "Score: \(score)", style: .plain, target: self, action: nil) navigationItem.setRightBarButtonItems([scoreButton], animated: false) }
override func viewDidLoad() { super.viewDidLoad() // Add the controls here since we need to make sure that the view is already loaded addSwipeGestures() addKeyCommands() }
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // Shows the instructions on how to control the snake at the beginning let instructionsController = UIAlertController(title: "Instructions", message: "To control the snake use your arrow keys on the keyboard or simply swipe in the direction you want to move.", preferredStyle: .alert) let start = UIAlertAction(title: "Got it!", style: .default) instructionsController.addAction(start) self.present(instructionsController, animated: true) }
// Control the snake via the keyboard keys private func addKeyCommands() { let commands: [UIKeyCommand] = [ UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(goUp)), UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(goDown)), UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(goLeft)), UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(goRight)), UIKeyCommand(input: "p", modifierFlags: [], action: #selector(pause)), ]
for command in commands { command.wantsPriorityOverSystemBehavior = true command.allowsAutomaticMirroring = false
addKeyCommand(command) } }
// Control the snake via swipe gestures func addSwipeGestures() { let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(goLeft)) swipeLeft.direction = .left view.addGestureRecognizer(swipeLeft)
let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(goRight)) swipeRight.direction = .right view.addGestureRecognizer(swipeRight)
let swipeUp = UISwipeGestureRecognizer(target: self, action: #selector(goUp)) swipeUp.direction = .up view.addGestureRecognizer(swipeUp)
let swipeDown = UISwipeGestureRecognizer(target: self, action: #selector(goDown)) swipeDown.direction = .down view.addGestureRecognizer(swipeDown) }
// MARK: Game States // Pause and restart function if you need to step away for a second @objc func pause() { if currentDirection != .none { previousDirection = currentDirection } currentDirection = .none let alertController = UIAlertController(title: "Game Paused", message: "", preferredStyle: .alert) let alert = UIAlertAction(title: "Resume", style: .default, handler: { _ in self.restart() }) alertController.addAction(alert) self.present(alertController, animated: true) }
@objc func restart() { currentDirection = previousDirection }
// For ending the game and resetting everything to the starting position func gameOver() { score = 0 currentDirection = .none currentGameState = .paused
let linesAndPolyLines = document?.annotationsForPage(at: pageIndex, type: [.line, .polyLine]) document!.remove(annotations: linesAndPolyLines!) createApple() updateScore() createSnake() }
// MARK: Timer func setupTimer() { if currentGameState == .paused { timer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true) } }
@objc func timerFired() { didBiteSelf() didEatApple() didLeaveArea() goDirection(direction: currentDirection) }
// MARK: Directions @objc func goUp() { if currentDirection == .down { return } currentDirection = .up setupTimer() }
@objc func goDown() { if currentDirection == .up { return } currentDirection = .down setupTimer() }
@objc func goLeft() { if currentDirection == .right { return } currentDirection = .left setupTimer() }
@objc func goRight() { if currentDirection == .left { return } currentDirection = .right setupTimer() }
func goDirection(direction: Direction) { var points = snake.points currentGameState = .playing switch direction { case .up: yEnd += increment case .down: yEnd -= increment case .left: xEnd -= increment case .right: xEnd += increment case .none: return }
points?.append(CGPoint(x: xEnd, y: yEnd)) points?.remove(at: 0) snake.points = points document?.documentProviderForPage(at: snake.pageIndex)?.annotationManager.update([snake], animated: true) }
// MARK: Create Objects func createSnake() { let snake = PolyLineAnnotation() let xStart = 70 let yStart = 10 xEnd = 70 yEnd = 210 var points = [CGPoint(x: xStart, y: yStart)] for i in 1...10 { points.append(CGPoint(x: xStart, y: yStart + increment * i)) } snake.points = points snake.color = .blue snake.lineWidth = 20 document?.add(annotations: [snake]) }
@objc func createApple () { let apple = LineAnnotation() // Page boundaries are 600x840 // Need to create them in 20 increments since it needs to allign exactly with the snake's path var randomX = Int.random(in: 0...41) var randomY = Int.random(in: 0...29) randomX *= 20 randomY *= 20 // We need the +10 because otherwise the point could be halfway outside the page, since it's starting point is in the middle of the annotation randomX += 10
// This will create a 20x20 square which is our apple apple.points = [CGPoint(x: randomX, y: randomY), CGPoint(x: randomX, y: randomY + 20)] apple.lineWidth = 20 apple.color = .red
document?.add(annotations: [apple]) }
// MARK: Actions func didEatApple() { guard let lastSnakePoint = snake.points?.last else { return } guard let lastApplePoint = apple.points?.last else { return } guard let firstApplePoint = apple.points?.first else { return }
// Since the apple is made from 2 points, we need to check if the head of our snake is between those if lastSnakePoint.y <= lastApplePoint.y && lastSnakePoint.y >= firstApplePoint.y && lastSnakePoint.x <= lastApplePoint.x && lastSnakePoint.x >= firstApplePoint.x { addTail() } else { return } score += 1 updateScore() document?.remove(annotations: (document?.annotationsForPage(at: pageIndex, type: .line))!) // Need to create a new one once the old one gets eaten createApple() }
func didBiteSelf() { guard let snakePoints = snake.points?.count else { return } guard let lastSnakePoint = snake.points?.last else { return }
// Check for each point in the snake. Can't coun't in our own head though! for snakePoint in 0...snakePoints - 2 { if lastSnakePoint.x == snake.points![snakePoint].x && lastSnakePoint.y == snake.points![snakePoint].y && currentGameState == .playing { timer.invalidate() let alertController = UIAlertController(title: "Ouch, don't bite yourself!", message: "", preferredStyle: .alert) let alert = UIAlertAction(title: "Restart Game", style: .default, handler: { _ in self.gameOver() }) alertController.addAction(alert) self.present(alertController, animated: true) currentDirection = .none } } }
func didLeaveArea() { guard let lastSnakePoint = snake.points?.last else { return } guard let documentHeight = document?.pageInfoForPage(at: pageIndex)?.size.height else { return } guard let documentWidth = document?.pageInfoForPage(at: pageIndex)?.size.width else { return }
if (lastSnakePoint.x >= documentWidth || lastSnakePoint.x < 0 || lastSnakePoint.y >= documentHeight || lastSnakePoint.y < 0) && currentGameState == .playing { timer.invalidate() let alertController = UIAlertController(title: "Don't leave the area!", message: "", preferredStyle: .alert) let alert = UIAlertAction(title: "Restart Game", style: .default, handler: { _ in self.gameOver() }) alertController.addAction(alert) self.present(alertController, animated: true) currentDirection = .none } }
func addTail() { // This is just a placeholder so the values don’t matter. This point will immediately be removed in `goDirection`. snake.points?.insert(CGPoint(x: 0, y: 0), at: 0) }
func updateScore() { navigationItem.rightBarButtonItem?.title = "Score: \(score)" }}
This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.