The Touch Controller framework lets you add fully customizable on-screen touch controls to your game that automatically surface as a GCController object, integrating directly with your existing Game Controller input pipeline and rendering via Metal.
โข Extends Game Controller framework support to touch input so existing controller-handling code works unchanged โ no new game logic required
โข Provides nine anchor points, safe-area APIs, and half-screen collider shapes so controls adapt gracefully across every iPhone and iPad screen size
โข Controls act as both input and output โ icons update contextually, controls hide when irrelevant, and on-screen buttons can replace overlay UIs entirely
Demonstrates how to create a TCTouchController, add a contextual action button with a system SF Symbol, expand a thumbstick to cover the full left half of the screen, and forward UIKit touch events โ all wiring into the existing GCController pipeline.
import UIKit
import TouchController
import GameController
import Metal
class GameViewController: UIViewController {
var touchController: TCTouchController!
var device: MTLDevice!
var commandQueue: MTLCommandQueue!
override func viewDidLoad() {
super.viewDidLoad()
device = MTLCreateSystemDefaultDevice()!
commandQueue = device.makeCommandQueue()!
setupTouchController()
observeControllerConnections()
}
// MARK: โ Touch Controller Setup
func setupTouchController() {
let descriptor = TCTouchControllerDescriptor()
touchController = TCTouchController(descriptor: descriptor)
touchController.connect() // Surfaces as a GCController automatically
addStrikeButton()
addMovementThumbstick()
}
func addStrikeButton() {
let buttonDesc = TCButtonDescriptor()
buttonDesc.label = .buttonB // Maps to GCController.buttonB
buttonDesc.anchor = .bottomRight
let safe = view.safeAreaInsets
buttonDesc.offset = CGPoint(x: -safe.right - 60, y: -safe.bottom - 60)
// SF Symbol icon that matches the current action
var contents = TCControlContents()
contents.symbolName = "bolt.fill"
buttonDesc.contents = contents
touchController.addButton(buttonDesc)
}
func addMovementThumbstick() {
let stickDesc = TCThumbstickDescriptor()
stickDesc.label = .leftThumbstick
stickDesc.anchor = .bottomLeft
stickDesc.hidesWhenNotPressed = true
// Expand hit area to entire left half of screen
stickDesc.colliderShape = .leftSide
touchController.addThumbstick(stickDesc)
}
// MARK: โ Contextual icon update (call when player selects power)
func updateStrikeButtonIcon(symbolName: String) {
var contents = TCControlContents()
contents.symbolName = symbolName
touchController.updateButton(label: .buttonB, contents: contents)
}
// MARK: โ Forward UIKit touches to the framework
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
touchController.handleTouchBegan(touches, with: event, in: view)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
touchController.handleTouchMoved(touches, with: event, in: view)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
touchController.handleTouchEnded(touches, with: event, in: view)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
touchController.handleTouchEnded(touches, with: event, in: view)
}
// MARK: โ GCController integration (unchanged from physical controller code)
func observeControllerConnections() {
NotificationCenter.default.addObserver(
forName: .GCControllerDidConnect,
object: nil,
queue: .main
) { [weak self] notification in
guard let controller = notification.object as? GCController else { return }
self?.bindGamepad(controller)
}
}
func bindGamepad(_ controller: GCController) {
controller.extendedGamepad?.buttonB.valueChangedHandler = { _, _, pressed in
if pressed { print("Strike / action triggered") }
}
}
// MARK: โ Metal render pass (call each frame inside your render loop)
func renderFrame() {
guard let drawable = (view.layer as? CAMetalLayer)?.nextDrawable(),
let commandBuffer = commandQueue.makeCommandBuffer() else { return }
// โฆ your existing scene render pass โฆ
// Overlay touch controls on top of the scene
touchController.render(commandBuffer: commandBuffer, drawable: drawable)
commandBuffer.present(drawable)
commandBuffer.commit()
}
}Liquid Glass is Apple's new material and visual design language introduced in iOS 27, bringing translucent, refractive glass-like surfaces to system and custom UI elements. It replaces the frosted vibrancy aesthetic with a more dynamic, depth-aware material that responds to content beneath it.
PaperKit is Apple's full-featured canvas framework โ previously internal-only โ now publicly available in iOS/macOS/visionOS 27. It powers the drawing and markup experience in Notes, Preview, and Freeform, giving developers access to a complete pencil, shapes, images, and text canvas with a rich data model.
Xcode 27 introduces a fully customizable toolbar and theme system, untitled scratch projects, coding agent integration directly in the editor, and the new Device Hub for evaluating apps across simulators and physical devices side-by-side.
In-depth guide
SwiftUI & Liquid Glass in iOS 27 โYou must forward UIKit touch events (touchesBegan, touchesMoved, touchesEnded, touchesCancelled) manually to the TCTouchController โ the framework does not swizzle UIKit. Safe-area insets must be applied manually to control offsets to avoid Dynamic Island and home indicator overlap. Controls are rendered in the Metal render pass so draw order matters.
Requires a Metal-capable device; touch input only meaningful on iPhone and iPad (not relevant on Mac without touch screen)
iOS 27 introduces new SwiftUI drag and drop APIs including reorderable, reorderContainer, dragContainer, and configuration modifiers that enable reordering within and across collections, multi-item drag, and fine-grained control over how data is transferred during drag and drop operations.