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.

⏱ 5 min read Updated for TilePix v1.99 Game Development / 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 offsetX and offsetY values 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

  1. In Xcode, go to File › Add Package Dependencies... and add ZIPFoundation.
  2. Export your project from TilePix as a ZIP archive, name it archive.zip, and add it to your Xcode project bundle.
  3. 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