The Touch Controller framework lets game developers add on-screen touch controls that surface as a GCController object, enabling the same game logic to work for both physical and touch input. Controls are fully customizable, support adaptive layouts with safe-area anchoring, and render directly via Metal for maximum performance.
⢠Enables iPhone/iPad players to enjoy games without a physical controller, dramatically expanding your addressable audience
⢠Touch controls appear as a standard GCController, so no duplicate input-handling code is required ā existing controller logic just works
⢠Dynamic show/hide and context-sensitive icons make touch controls feel native rather than a bolted-on afterthought
Sets up a Touch Controller with a full-screen left thumbstick and a context-sensitive action button that updates its icon when the active power changes, mirroring a GCController layout.
import UIKit
import TouchController
import GameController
import Metal
class GameViewController: UIViewController {
var touchController: TCTouchController?
var actionButton: TCButton?
var device: MTLDevice!
var commandQueue: MTLCommandQueue!
override func viewDidLoad() {
super.viewDidLoad()
device = MTLCreateSystemDefaultDevice()!
commandQueue = device.makeCommandQueue()!
setupTouchController()
}
func setupTouchController() {
let descriptor = TCTouchControllerDescriptor()
let tc = TCTouchController(descriptor: descriptor)
tc.connect()
self.touchController = tc
let insets = view.safeAreaInsets
// Full-screen left thumbstick
let stickDesc = TCThumbstickDescriptor()
stickDesc.label = .leftThumbstick
stickDesc.anchor = .bottomLeft
stickDesc.offset = CGPoint(x: insets.left + 20, y: -(insets.bottom + 20))
stickDesc.colliderShape = .leftSide // entire left half is the touch zone
stickDesc.hidesWhenNotPressed = true
tc.addThumbstick(stickDesc)
// Context-sensitive action button
let btnDesc = TCButtonDescriptor()
btnDesc.label = .buttonA
btnDesc.anchor = .bottomRight
btnDesc.offset = CGPoint(x: -(insets.right + 80), y: -(insets.bottom + 30))
let cfg = TCButtonVisualConfiguration()
cfg.systemSymbolName = "bolt.fill"
btnDesc.visualConfiguration = cfg
let button = tc.addButton(btnDesc)
self.actionButton = button
// Poll controller state via GCController interface
tc.controller.extendedGamepad?.valueChangedHandler = { [weak self] pad, element in
if let btn = pad.buttonA, btn.isPressed {
self?.handleActionPressed()
}
}
}
// Call this whenever the active power changes
func updateActionButtonIcon(symbolName: String) {
guard let button = actionButton else { return }
let cfg = TCButtonVisualConfiguration()
cfg.systemSymbolName = symbolName
button.visualConfiguration = cfg
}
func handleActionPressed() {
// Game logic here ā same code path used for physical GCController
print("Action triggered via touch or physical controller")
}
// MARK: - UIKit touch forwarding
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
touchController?.handleTouchesBegan(touches, with: event, in: view)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
touchController?.handleTouchesMoved(touches, with: event, in: view)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
touchController?.handleTouchesEnded(touches, with: event, in: view)
}
// MARK: - Metal render pass (call each frame)
func renderFrame(drawable: CAMetalDrawable) {
guard let cmdBuffer = commandQueue.makeCommandBuffer() else { return }
// ... your game render commands ...
touchController?.render(with: cmdBuffer, drawable: drawable)
cmdBuffer.present(drawable)
cmdBuffer.commit()
}
}Safe-area insets must be manually applied to control offsets or controls can be obscured by the Dynamic Island or home indicator. colliderShape must be set to leftSide/rightSide to give thumbsticks adequate touch target area ā the default visual-only hit area is too small for comfortable play. Controls set to isEnabled = false are hidden but still consume memory; remove infrequently used controls entirely with removeButton when not needed.
Requires a device with a touchscreen; optimized for iPhone and iPad. Physical controller fallback recommended for Apple TV.
More iOS 27 APIs land every week.
Get notified when new capabilities are published ā no noise, just signal.