Import TilePix Maps into SpriteKit (Swift) v1.99
A complete guide to loading TilePix 1.99 multi-layered tilemaps, custom game properties, and keyframed tile animations into iOS and macOS games using SpriteKit and Swift.
Contents
Overview
TilePix 1.99 allows you to design 2D pixel art tilesets, paint multi-layered tilemaps, assign custom game properties (like isSolid or playerSpawn), and create sequenced tile animations with precise Travel Duration, Hold Duration, and X/Y tile offsets.
When you export your project as a standard .zip bundle from TilePix, it packages your layered map definitions, tileset metadata, and graphics sheet. Below is a complete, copy-pasteable integration script using SpriteKit, SwiftUI, and ZIPFoundation to parse and render your maps and animations in real time.
What's New in TilePix 1.99
- Dual-Phase Timing: Separate control for Travel Time (smooth motion transition) and Hold Time (resting pause) per frame.
- Offset Traversal: Independent
offsetXandoffsetYvalues for moving floating platforms, animated hazards, and environmental effects. - Zero-Glide Loop Snap: Clean, one-shot reset to Frame 1 on loop completion without reverse interpolation.
- Custom Tile Properties: Typed metadata classes (
bool,string,int,double) decoded directly into your game entities.
What TilePix Exports
Map.json: Contains project structure, map layer GIDs, animation timelines, and custom property dictionaries.tileset.png: The master pixel-art tilesheet texture.- Full compatibility with Tiled (
.tmj/.tsj) and native TilePix bundles.
Prerequisites & Setup
- In Xcode, go to File › Add Package Dependencies... and add ZIPFoundation.
- Export your project from TilePix as a ZIP archive, name it
archive.zip, and add it to your Xcode project bundle. - Copy the Swift code below into your game project.
Complete Code Implementation (SpriteKit + TilePix 1.99)
import SwiftUI
import SpriteKit
import ZIPFoundation
// MARK: - 1. TilePix 1.99 Data Models
struct GameProject: Codable {
let id: String
let name: String
let maps: [GameMap]
let tilesets: [Tileset]
}
struct Tileset: Codable {
let id: String
let name: String
let tileWidth: Int
let tileHeight: Int
let grid: Grid
let tileClasses: [String: TileClass]?
let animations: [String: TileAnimation]?
}
struct Grid: Codable {
let width: Int
let height: Int
}
struct TileClass: Codable {
let id: Int?
let name: String?
let properties: [String: TileProperty]?
}
struct TileProperty: Codable {
let type: String
let value: AnyCodableValue
var boolValue: Bool? { if case .bool(let v) = value { return v }; return nil }
var stringValue: String? { if case .string(let v) = value { return v }; return nil }
var intValue: Int? { if case .int(let v) = value { return v }; return nil }
}
enum AnyCodableValue: Codable {
case bool(Bool), int(Int), string(String), double(Double)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let b = try? container.decode(Bool.self) { self = .bool(b) }
else if let i = try? container.decode(Int.self) { self = .int(i) }
else if let d = try? container.decode(Double.self) { self = .double(d) }
else if let s = try? container.decode(String.self) { self = .string(s) }
else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid value") }
}
func encode(to encoder: Encoder) throws {}
}
struct TileAnimation: Codable {
let frames: [TileAnimationFrame]
let isLooping: Bool?
let groupID: String?
}
struct TileAnimationFrame: Codable {
let id: String?
let tileID: Int
let duration: TimeInterval // Travel duration in seconds
let holdDuration: TimeInterval? // Hold duration in seconds
let offsetX: CGFloat? // X offset in tile units
let offsetY: CGFloat? // Y offset in tile units
}
struct GameMap: Codable {
let id: String
let name: String?
let layers: [MapLayer]
let mapWidthInTiles: Int
let mapHeightInTiles: Int
let associatedTilesetID: String
}
struct MapLayer: Codable {
let id: String
let name: String?
let tileData: [Int] // -1 represents empty tile
let isVisible: Bool?
}
// MARK: - 2. SpriteKit Map & Animation Scene
class TilePixScene: SKScene {
private let mapContainer = SKNode()
private let cameraNode = SKCameraNode()
private var project: GameProject?
private var cachedTileTextures: [Int: SKTexture] = [:]
private var cachedTileSets: [String: SKTileSet] = [:]
private var cachedTileGroups: [String: [Int: SKTileGroup]] = [:]
override func didMove(to view: SKView) {
backgroundColor = .black
addChild(mapContainer)
addChild(cameraNode)
camera = cameraNode
// Load the exported TilePix zip file from the app bundle
if let zipURL = Bundle.main.url(forResource: "archive", withExtension: "zip") {
loadTilePixProject(from: zipURL)
}
}
// MARK: - Loader
func loadTilePixProject(from url: URL) {
guard let archive = Archive(url: url, accessMode: .read) else {
print("❌ Failed to open zip archive.")
return
}
// Extract JSON Project file
guard let jsonEntry = archive.first(where: { $0.path.hasSuffix(".json") && !$0.path.contains("__MACOSX") }) else { return }
var jsonData = Data()
_ = try? archive.extract(jsonEntry) { jsonData.append($0) }
guard let project = try? JSONDecoder().decode(GameProject.self, from: jsonData) else {
print("❌ Failed to decode TilePix project JSON.")
return
}
self.project = project
// Extract Tileset PNG
guard let imgEntry = archive.first(where: { $0.path.hasSuffix("tileset.png") && !$0.path.contains("__MACOSX") }) else { return }
var imgData = Data()
_ = try? archive.extract(imgEntry) { imgData.append($0) }
guard let uiImage = UIImage(data: imgData) else { return }
let atlasTexture = SKTexture(image: uiImage)
atlasTexture.filteringMode = .nearest
// Build SKTileSets & Sub-textures
buildTilesetCache(tilesets: project.tilesets, atlasTexture: atlasTexture)
// Render the primary map
if let firstMap = project.maps.first {
renderMap(firstMap)
}
}
private func buildTilesetCache(tilesets: [Tileset], atlasTexture: SKTexture) {
for ts in tilesets {
let cols = ts.grid.width / ts.tileWidth
let rows = ts.grid.height / ts.tileHeight
let sW = CGFloat(ts.grid.width)
let sH = CGFloat(ts.grid.height)
let tW = CGFloat(ts.tileWidth)
let tH = CGFloat(ts.tileHeight)
var groups: [SKTileGroup] = []
var groupDict: [Int: SKTileGroup] = [:]
var tileID = 0
for r in 0..= 0 else { continue }
let skRow = map.mapHeightInTiles - 1 - row
// Check if this tile has an animation sequence
if let anim = ts.animations?["\(tileID)"], !anim.frames.isEmpty {
spawnAnimatedTile(
baseTileID: tileID,
animation: anim,
col: col,
row: skRow,
tileSize: tileSize,
zPosition: tileMapNode.zPosition + 1
)
} else if let group = cachedTileGroups[ts.id]?[tileID] {
// Static tile
tileMapNode.setTileGroup(group, forColumn: col, row: skRow)
}
}
}
mapContainer.addChild(tileMapNode)
}
}
// MARK: - Animated Tile Engine (Travel, Hold, & Offset Interpolation)
private func spawnAnimatedTile(
baseTileID: Int,
animation: TileAnimation,
col: Int,
row: Int,
tileSize: CGFloat,
zPosition: CGFloat
) {
let initialTileID = animation.frames.first?.tileID ?? baseTileID
let initialTexture = cachedTileTextures[initialTileID] ?? SKTexture()
let sprite = SKSpriteNode(texture: initialTexture)
sprite.size = CGSize(width: tileSize, height: tileSize)
let basePosition = CGPoint(x: CGFloat(col) * tileSize + tileSize / 2, y: CGFloat(row) * tileSize + tileSize / 2)
sprite.position = basePosition
sprite.zPosition = zPosition
mapContainer.addChild(sprite)
var actions: [SKAction] = []
let frames = animation.frames
for (i, frame) in frames.enumerated() {
let prevIndex = (i == 0) ? 0 : i - 1
let prevFrame = frames[prevIndex]
let frameTexture = cachedTileTextures[frame.tileID] ?? initialTexture
let startOffset = CGPoint(x: (prevFrame.offsetX ?? 0.0) * tileSize, y: (prevFrame.offsetY ?? 0.0) * tileSize)
let endOffset = CGPoint(x: (frame.offsetX ?? 0.0) * tileSize, y: (frame.offsetY ?? 0.0) * tileSize)
let startPos = CGPoint(x: basePosition.x + startOffset.x, y: basePosition.y + startOffset.y)
let endPos = CGPoint(x: basePosition.x + endOffset.x, y: basePosition.y + endOffset.y)
// 1. Texture swap & Loop snap on Frame 1
let swapAction = SKAction.run { [weak sprite] in
sprite?.texture = frameTexture
if i == 0 { sprite?.position = startPos }
}
actions.append(swapAction)
// 2. Travel Duration (Smooth Move)
let travelTime = max(0.0, frame.duration)
if travelTime > 0 {
actions.append(SKAction.move(to: endPos, duration: travelTime))
} else {
actions.append(SKAction.run { [weak sprite] in sprite?.position = endPos })
}
// 3. Hold Duration (Stay in position)
let holdTime = max(0.0, frame.holdDuration ?? 0.0)
if holdTime > 0 {
actions.append(SKAction.wait(forDuration: holdTime))
}
}
guard !actions.isEmpty else { return }
let fullSequence = SKAction.sequence(actions)
if animation.isLooping ?? true {
sprite.run(SKAction.repeatForever(fullSequence))
} else {
sprite.run(fullSequence)
}
}
}
// MARK: - 3. SwiftUI Preview Container
struct ContentView: View {
var scene: SKScene {
let sc = TilePixScene()
sc.size = CGSize(width: 400, height: 400)
sc.scaleMode = .resizeFill
return sc
}
var body: some View {
SpriteView(scene: scene)
.ignoresSafeArea()
}
}
#Preview {
ContentView()
}
Demo:
Example of tile flipping, floating platforms, and sequenced character movement with independent travel and hold durations.
Why This Architecture Works Well
Dual-Phase Animations
SpriteKit SKAction chains handle travel easing and hold delays cleanly on the GPU without frame jitter.
Pixel Accuracy
Nearest-neighbour filtering keeps retro pixel art and sub-pixel keyframes sharp on Retina screens.
Custom Properties
Decodes collision classes, spawn tags, and item triggers automatically into type-safe Swift enums.
Multi-Layer Depth
Maintains bottom-to-top layer hierarchies and visibility flags for parallax and foreground rendering.
Build Your Game Faster with TilePix 1.99
Design tilesets, paint layered maps, configure motion timelines, and export directly into SpriteKit.
Download TilePix on the App Store