Swash

1.2.0

Entity Component Systems framework for Swift
johnrnyquist/Swash

What's New

Swash 1.2.0

2026-06-14T18:13:14Z

This release makes Swash's engine diagnostics configurable. The engine's informational notices are now gated behind a log-level toggle so they no longer print unconditionally — quiet by default in shipping apps, while still surfacing genuine warnings during development.

Highlights

  • Added SwashLog, a configurable diagnostic logger with a Level (none/error/warning/info/debug) and a swappable output handler.
  • Engine notices that were previously unconditional print() calls now route through SwashLog.
  • Sensible defaults: .warning under DEBUG, .none in release — so a linked app stays silent unless it opts in with SwashLog.level = .debug.

Added

  • Sources/Swash/core/SwashLog.swift

Changed

  • Engine.removeEntities(named:) "did not contain entity" notice is now logged at .info.
  • Entity.remove(componentClass:) "does not exist" notice is now logged at .info.
  • SystemList.add(system:) duplicate-add notice is now logged at .warning.

Compatibility Notes

  • Additive public API — SwashLog is new; no existing symbols were changed or removed. Source-compatible with 1.1.0.
  • One intentional behavior change: those three notices no longer print unconditionally. In release they are silent by default; under DEBUG the two .info notices are suppressed while .warning still prints. Consumers relying on the old console output can restore it with SwashLog.level = .debug, or redirect it via SwashLog.handler.

Stats Since 1.1.0

  • 1 commit
  • 4 files changed
  • 55 insertions, 3 deletions

Swash logo

Swash

A lightweight, fast Entity Component System framework for Swift.

Swash brings the ECS model from Ash into strongly typed Swift, with a small API surface and predictable runtime behavior. It powers real projects, including Swashteroids.

Why Swash

  • Built for game loops and real-time simulation
  • Clean separation of data (Components) and behavior (Systems)
  • Automatic entity-to-node matching via Engine + Family
  • Small framework footprint, easy to reason about
  • Battle-tested in a shipping iOS game

What Is ECS (In Swash Terms)

  • Entity: a container of components
  • Component: plain data, no game logic
  • Node: a typed view over required components
  • System: logic that updates nodes each frame
  • Engine: owns entities/systems and runs updates

If you are coming from OOP, ECS can feel unfamiliar at first. That is normal. Once it clicks, complex game behavior gets easier to scale and test.

Installation (Swift Package Manager)

Add Swash to your Package.swift dependencies:

dependencies: [
	.package(url: "https://github.com/johnrnyquist/Swash.git", from: "1.0.0")
]

Then add Swash to your target dependencies:

.target(
	name: "YourGame",
	dependencies: ["Swash"]
)

Quick Start

1) Define Components

import Swash

final class PositionComponent: Component {
	var x: Double
	var y: Double

	init(x: Double = 0, y: Double = 0) {
		self.x = x
		self.y = y
		super.init()
	}
}

final class VelocityComponent: Component {
	var dx: Double
	var dy: Double

	init(dx: Double = 0, dy: Double = 0) {
		self.dx = dx
		self.dy = dy
		super.init()
	}
}

2) Define a Node

A Node declares which components a System needs.

import Swash

final class MovementNode: Node {
	required init() {
		super.init()
		components = [
			PositionComponent.name: nil,
			VelocityComponent.name: nil,
		]
	}
}

3) Define a System

Use ListIteratingSystem when you want to iterate over matching nodes.

import Foundation
import Swash

final class MovementSystem: ListIteratingSystem {
	init() {
		super.init(nodeClass: MovementNode.self)
		nodeUpdateFunction = updateNode
	}

	private func updateNode(node: Node, time: TimeInterval) {
		guard let position = node[PositionComponent.self],
			  let velocity = node[VelocityComponent.self]
		else { return }

		position.x += velocity.dx * time
		position.y += velocity.dy * time
	}
}

4) Build and Run

import Swash

let engine = Engine(name: "main")

engine.add(system: MovementSystem(), priority: 10)

let player = Entity(named: "player")
	.add(component: PositionComponent(x: 0, y: 0))
	.add(component: VelocityComponent(dx: 100, dy: 0))

engine.add(entity: player)

// Typically called from your frame/tick provider.
engine.update(time: 1.0 / 60.0)

Core API At A Glance

Engine

  • add(entity:)
  • remove(entity:)
  • removeAllEntities()
  • add(system:priority:)
  • remove(system:)
  • removeAllSystems()
  • getNodeList(nodeClassType:)
  • releaseNodeList(nodeClassName:)
  • update(time:)

Entity

  • add(component:)
  • remove(componentClass:)
  • find(componentClass:)
  • has(componentClass:)
  • subscript(componentClass:)
  • subscript(componentName:)

System lifecycle

  • addToEngine(engine:)
  • update(time:)
  • removeFromEngine(engine:)

Update Order and Priorities

Systems run in ascending priority order.

  • Lower number = runs earlier
  • Higher number = runs later

Example ordering:

  • InputSystem priority 0
  • MovementSystem priority 10
  • CollisionSystem priority 20
  • RenderPrepSystem priority 30

Practical Notes

  • Adding an Entity with a duplicate name replaces the existing one in Engine.
  • Families and NodeList instances are created lazily when requested.
  • releaseNodeList can free memory/resources when a list is no longer needed.
  • During Engine updates, internal node pooling helps reduce churn.

Example Projects

Swashteroids

Origins

Swash is inspired by the Ash ECS framework by Richard Lord, adapted to Swift's stronger type system.

Status

Swash is stable and in active use, with room to evolve.

If you are building something with it, open an issue or discussion and share what you are making.

Author

John Nyquist
https://linkedin.com/in/nyquist

License

MIT
See LICENSE.md

Description

  • Swift Tools 5.9.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sun Nov 01 2026 18:01:22 GMT-1000 (Hawaii-Aleutian Standard Time)