Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Advanced SpriteKit patterns for tile maps, texture atlases, shader effects, scene transitions, game loop architecture, audio, and SceneKit embedding.
SKTileMapNode renders a grid of tile images. Define tile sets in Xcode's
SpriteKit scene editor or build them programmatically. Tile maps support
square, hexagonal, and isometric grids.
func createTileMap() -> SKTileMapNode {
// Create tile definitions
let grassTexture = SKTexture(imageNamed: "grass")
let grassDef = SKTileDefinition(texture: grassTexture, size: CGSize(width: 32, height: 32))
let dirtTexture = SKTexture(imageNamed: "dirt")
let dirtDef = SKTileDefinition(texture: dirtTexture, size: CGSize(width: 32, height: 32))
// Create tile groups
let grassGroup = SKTileGroup(tileDefinition: grassDef)
let dirtGroup = SKTileGroup(tileDefinition: dirtDef)
// Create tile set
let tileSet = SKTileSet(tileGroups: [grassGroup, dirtGroup])
// Create tile map
let tileMap = SKTileMapNode(
tileSet: tileSet,
columns: 20,
rows: 15,
tileSize: CGSize(width: 32, height: 32)
)
tileMap.fill(with: grassGroup)
return tileMap
}// Set a specific tile
tileMap.setTileGroup(dirtGroup, forColumn: 5, row: 3)
// Set tile with a specific definition (for adjacency rules)
tileMap.setTileGroup(dirtGroup, andTileDefinition: dirtDef, forColumn: 5, row: 3)Convert between tile grid coordinates and scene positions:
// Scene position to tile coordinate
let column = tileMap.tileColumnIndex(fromPosition: touchLocation)
let row = tileMap.tileRowIndex(fromPosition: touchLocation)
// Tile coordinate to scene position
let center = tileMap.centerOfTile(atColumn: column, row: row)Tile maps do not expose individual tiles as nodes. Overlay invisible
SKNode objects for physics:
func addPhysicsToTileMap(_ tileMap: SKTileMapNode) {
let tileSize = tileMap.tileSize
for column in 0..<tileMap.numberOfColumns {
for row in 0..<tileMap.numberOfRows {
guard let def = tileMap.tileDefinition(atColumn: column, row: row),
def.userData?["isWall"] as? Bool == true else { continue }
let center = tileMap.centerOfTile(atColumn: column, row: row)
let wall = SKNode()
wall.position = center
wall.physicsBody = SKPhysicsBody(rectangleOf: tileSize)
wall.physicsBody?.isDynamic = false
wall.physicsBody?.categoryBitMask = PhysicsCategory.wall
tileMap.addChild(wall)
}
}
}Use SKTileGroupRule with adjacency masks for auto-tiling (e.g., terrain
edges that blend into neighboring terrain):
let centerRule = SKTileGroupRule(
adjacency: .adjacencyAll,
tileDefinitions: [centerDef]
)
let topEdgeRule = SKTileGroupRule(
adjacency: .adjacencyUpEdge,
tileDefinitions: [topEdgeDef]
)
let tileGroup = SKTileGroup(rules: [centerRule, topEdgeRule])Enable auto-fill by using tileMap.enableAutomapping = true in the scene
editor. The tile map picks the correct definition based on neighboring tiles.
Texture atlases pack multiple images into a single texture, reducing draw calls and improving GPU performance.
.atlas extension to the asset catalog.let atlas = SKTextureAtlas(named: "Characters")
let textures = atlas.textureNames.sorted().map { atlas.textureNamed($0) }
let animation = SKAction.animate(with: textures, timePerFrame: 0.1)
sprite.run(SKAction.repeatForever(animation))Preload atlases before presenting a scene to avoid frame drops during first use:
SKTextureAtlas.preloadTextureAtlasesNamed(["Characters", "Environment"]) {
error, atlases in
// Atlases are now in GPU memory; present the scene.
presentGameScene()
}Alternatively, preload using async:
func preloadAssets() async {
await withCheckedContinuation { continuation in
SKTextureAtlas.preloadTextureAtlases([
SKTextureAtlas(named: "Characters"),
SKTextureAtlas(named: "Environment")
]) {
continuation.resume()
}
}
}let texture = SKTexture(imageNamed: "pixel_art")
texture.filteringMode = .nearest // Sharp pixels for pixel art
// .linear (default) for smooth scalingSKTransition provides animated transitions between scenes. Present the
new scene with a transition through the view:
func goToGameOver() {
let gameOverScene = GameOverScene(size: size)
gameOverScene.scaleMode = scaleMode
let transition = SKTransition.fade(withDuration: 1.0)
view?.presentScene(gameOverScene, transition: transition)
}// Fade
SKTransition.fade(withDuration: 1.0)
SKTransition.fade(with: .black, duration: 1.0)
// Slide
SKTransition.push(with: .left, duration: 0.5)
SKTransition.moveIn(with: .right, duration: 0.5)
SKTransition.reveal(with: .down, duration: 0.5)
// Dissolve effects
SKTransition.crossFade(withDuration: 1.0)
SKTransition.flipHorizontal(withDuration: 0.5)
SKTransition.flipVertical(withDuration: 0.5)
// Doors
SKTransition.doorway(withDuration: 1.0)
SKTransition.doorsOpenHorizontal(withDuration: 0.5)
SKTransition.doorsOpenVertical(withDuration: 0.5)
SKTransition.doorsCloseHorizontal(withDuration: 0.5)
SKTransition.doorsCloseVertical(withDuration: 0.5)By default, both the outgoing and incoming scenes run during a transition. Pause the outgoing scene if needed:
let transition = SKTransition.fade(withDuration: 1.0)
transition.pausesOutgoingScene = true
transition.pausesIncomingScene = falseFrame-rate-independent movement requires delta time calculation:
final class GameScene: SKScene {
private var lastUpdateTime: TimeInterval = 0
override func update(_ currentTime: TimeInterval) {
let deltaTime: TimeInterval
if lastUpdateTime == 0 {
deltaTime = 0
} else {
deltaTime = currentTime - lastUpdateTime
}
lastUpdateTime = currentTime
updateEntities(deltaTime: deltaTime)
}
}Organize game objects using a simple entity-component structure. For complex
games, consider GameplayKit's GKEntity and GKComponent.
protocol GameComponent {
func update(deltaTime: TimeInterval)
}
final class HealthComponent: GameComponent {
var hitPoints: Int
var maxHitPoints: Int
init(hitPoints: Int) {
self.hitPoints = hitPoints
self.maxHitPoints = hitPoints
}
func update(deltaTime: TimeInterval) { }
func takeDamage(_ amount: Int) {
hitPoints = max(0, hitPoints - amount)
}
}
final class MovementComponent: GameComponent {
weak var node: SKNode?
var velocity: CGVector = .zero
func update(deltaTime: TimeInterval) {
guard let node else { return }
node.position.x += velocity.dx * deltaTime
node.position.y += velocity.dy * deltaTime
}
}Use SKAction for timed spawning rather than manual timer tracking:
func startSpawning() {
let spawn = SKAction.run { [weak self] in
self?.spawnEnemy()
}
let delay = SKAction.wait(forDuration: 2.0, withRange: 1.0) // 1.5-2.5s
run(SKAction.repeatForever(SKAction.sequence([spawn, delay])), withKey: "spawning")
}
func stopSpawning() {
removeAction(forKey: "spawning")
}Use SKSceneDelegate to share update logic across scenes without
subclassing:
final class GameController: SKSceneDelegate {
func update(_ currentTime: TimeInterval, for scene: SKScene) {
// Shared game logic applied to any scene
}
func didEvaluateActions(for scene: SKScene) { }
func didSimulatePhysics(for scene: SKScene) { }
}
// Usage
let scene = SKScene(fileNamed: "Level1")!
scene.delegate = gameControllerSKAudioNode provides positional audio tied to a node's position in the
scene. Set the scene's listener property for spatial audio.
// Background music
let music = SKAudioNode(fileNamed: "background.mp3")
music.autoplayLooped = true
music.isPositional = false
addChild(music)
// Positional sound effect
let engineSound = SKAudioNode(fileNamed: "engine.wav")
engineSound.isPositional = true
engineSound.autoplayLooped = true
spaceship.addChild(engineSound)
// Set the listener for positional audio
listener = cameraNodeFor short, non-positional sound effects:
let playSound = SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false)
run(playSound)This is simple but offers no volume or positional control. Use SKAudioNode
for sounds that need spatial positioning or dynamic volume.
// Stop a specific audio node
music.run(SKAction.changeVolume(to: 0, duration: 1.0)) {
music.removeFromParent()
}
// Or immediately
music.removeFromParent()SKLightNode adds 2D lighting with shadows to a scene. Light affects
sprites that have matching lightingBitMask values.
let light = SKLightNode()
light.categoryBitMask = 0b0001
light.falloff = 1.5
light.ambientColor = UIColor(white: 0.3, alpha: 1.0)
light.lightColor = .white
light.shadowColor = UIColor(white: 0.0, alpha: 0.5)
light.position = player.position
addChild(light)
// Enable lighting on a sprite
wall.lightingBitMask = 0b0001
wall.shadowCastBitMask = 0b0001 // This sprite casts shadows
wall.shadowedBitMask = 0b0001 // This sprite receives shadowsApply a normal map texture to a sprite for per-pixel lighting detail:
let sprite = SKSpriteNode(imageNamed: "stone_wall")
sprite.normalTexture = SKTexture(imageNamed: "stone_wall_normal")
sprite.lightingBitMask = 0b0001SKShader applies custom GLSL fragment shaders to sprites, shape nodes,
emitters, and tile maps.
let shader = SKShader(source: """
void main() {
vec2 uv = v_tex_coord;
vec4 color = texture2D(u_texture, uv);
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(vec3(gray), color.a) * v_color_mix;
}
""")
sprite.shader = shaderPass values from Swift to the shader:
let shader = SKShader(fileNamed: "dissolve.fsh")
shader.uniforms = [
SKUniform(name: "u_threshold", float: 0.5)
]
sprite.shader = shader
// Update at runtime
shader.uniformNamed("u_threshold")?.floatValue = newValue| Variable | Type | Description |
|---|---|---|
u_texture | sampler2D | The node's texture |
u_time | float | Time since shader attached |
u_path_length | float | Path length (shape nodes) |
v_tex_coord | vec2 | Texture coordinate |
v_color_mix | vec4 | Node's blend color |
u_sprite_size | vec2 | Sprite size in points |
Pass per-node values through attributes when multiple nodes share a shader but need different parameters:
let shader = SKShader(fileNamed: "tint.fsh")
shader.attributes = [
SKAttribute(name: "a_tintColor", type: .vectorFloat4)
]
sprite.setValue(
SKAttributeValue(vectorFloat4: vector_float4(1, 0, 0, 1)),
forAttribute: "a_tintColor"
)SKConstraint limits a node's position or rotation automatically each frame.
// Keep node oriented toward a target
let orient = SKConstraint.orient(to: targetNode, offset: SKRange(constantValue: 0))
turret.constraints = [orient]
// Keep node within a rectangular boundary
let xRange = SKRange(lowerLimit: 50, upperLimit: frame.width - 50)
let yRange = SKRange(lowerLimit: 50, upperLimit: frame.height - 50)
let boundary = SKConstraint.positionX(xRange, y: yRange)
player.constraints = [boundary]
// Keep distance from another node
let distance = SKConstraint.distance(SKRange(lowerLimit: 50, upperLimit: 200), to: leader)
follower.constraints = [distance]Joints connect two physics bodies. Both bodies must exist in the scene before creating a joint.
// Pin joint: bodies rotate around a shared anchor
let pin = SKPhysicsJointPin.joint(
withBodyA: wheelBody,
bodyB: chassisBody,
anchor: wheelNode.position
)
physicsWorld.add(pin)
// Spring joint: elastic connection
let spring = SKPhysicsJointSpring.joint(
withBodyA: bodyA.physicsBody!,
bodyB: bodyB.physicsBody!,
anchorA: bodyA.position,
anchorB: bodyB.position
)
spring.frequency = 1.0
spring.damping = 0.5
physicsWorld.add(spring)
// Fixed joint: rigid connection
let fixed = SKPhysicsJointFixed.joint(
withBodyA: partA.physicsBody!,
bodyB: partB.physicsBody!,
anchor: CGPoint(x: 0, y: 0)
)
physicsWorld.add(fixed)
// Sliding joint: constrained to an axis
let slide = SKPhysicsJointSliding.joint(
withBodyA: slider.physicsBody!,
bodyB: track.physicsBody!,
anchor: slider.position,
axis: CGVector(dx: 1, dy: 0)
)
physicsWorld.add(slide)
// Limit joint: maximum distance between anchors
let limit = SKPhysicsJointLimit.joint(
withBodyA: chainLink1.physicsBody!,
bodyB: chainLink2.physicsBody!,
anchorA: chainLink1.position,
anchorB: chainLink2.position
)
physicsWorld.add(limit)Remove joints with physicsWorld.remove(joint).
SKFieldNode applies forces to physics bodies within a region. Bodies opt
in through fieldBitMask.
// Radial gravity (black hole effect)
let gravity = SKFieldNode.radialGravityField()
gravity.strength = 5.0
gravity.falloff = 1.0
gravity.position = CGPoint(x: frame.midX, y: frame.midY)
gravity.region = SKRegion(radius: 200)
addChild(gravity)
// Vortex (swirling)
let vortex = SKFieldNode.vortexField()
vortex.strength = 2.0
// Turbulence (random jitter)
let turbulence = SKFieldNode.turbulenceField(withSmoothness: 0.5, animationSpeed: 1.0)
// Linear gravity (wind)
let wind = SKFieldNode.linearGravityField(withVector: vector_float3(2, 0, 0))
wind.strength = 3.0
// Electric field (attracts/repels based on charge)
let electric = SKFieldNode.electricField()
// Set physicsBody.charge on affected bodiesBodies interact with fields when their fieldBitMask matches the field
node's categoryBitMask.
Masks child content using another node as a mask shape:
let cropNode = SKCropNode()
let maskShape = SKSpriteNode(imageNamed: "circle_mask")
cropNode.maskNode = maskShape
cropNode.addChild(contentSprite)
addChild(cropNode)Only the portions of children that overlap the mask's non-transparent pixels are rendered.
Applies a Core Image filter to its child subtree:
let effectNode = SKEffectNode()
effectNode.shouldRasterize = true // Cache the result for performance
effectNode.filter = CIFilter(name: "CIGaussianBlur", parameters: [
"inputRadius": 10.0
])
effectNode.addChild(backgroundSprite)
addChild(effectNode)Use shouldRasterize = true when children do not change frequently.
SK3DNode embeds a SceneKit scene within a SpriteKit scene:
let node3D = SK3DNode(viewportSize: CGSize(width: 200, height: 200))
let scnScene = SCNScene(named: "model.scn")!
node3D.scnScene = scnScene
// Set a camera for the 3D viewport
let scnCamera = SCNCamera()
let cameraNode = SCNNode()
cameraNode.camera = scnCamera
cameraNode.position = SCNVector3(x: 0, y: 2, z: 5)
scnScene.rootNode.addChild(cameraNode)
node3D.pointOfView = cameraNode
node3D.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(node3D)The 3D node participates in the 2D scene's draw order like any other node.
ignoresSiblingOrder = true on SKView to enable automatic batching.SKShapeNode for repeated elements; convert shapes to textures using
SKView.texture(from:).SKCameraNode.containedNodeSet() or manual
bounds checking to cull nodes that leave the viewport.SKAction.removeFromParent() in action sequences for projectiles and
effects that leave the screen.final class NodePool<T: SKNode> {
private var available: [T] = []
func acquire() -> T {
if let node = available.popLast() {
return node
}
return T()
}
func release(_ node: T) {
node.removeAllActions()
node.removeFromParent()
available.append(node)
}
}circleOfRadius, rectangleOf) over texture:size:
bodies when possible.usesPreciseCollisionDetection = true only on fast-moving small bodies
that tunnel through thin obstacles.isDynamic = false on static scenery.particleBirthRate and particleLifetime to the minimum needed.numParticlesToEmit for finite effects and remove the emitter after
completion.advanceSimulationTime(_:) to pre-warm emitters that should appear
mid-effect when added to the scene.targetNode to the scene when particles should detach from a moving
emitter, but be aware this prevents batching.SKTextureAtlas.preloadTextureAtlases to load textures before the
scene appears, avoiding mid-game stalls.Enable debug overlays on SKView during development:
skView.showsFPS = true
skView.showsNodeCount = true
skView.showsDrawCount = true
skView.showsPhysics = trueUse Instruments with the SpriteKit template to profile frame time, draw calls, and node count over time. The Core Animation instrument helps identify GPU bottlenecks.
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references