ChessboardKit is a SwiftUI library for rendering chessboards and playing chess on the board. It provides a highly customizable and interactive chessboard component for your SwiftUI applications.
| iOS | macOS |
|---|---|
| Promotion Picker | Suits its container's size |
|---|---|
| Hint | Move Legality Check |
|---|---|
ChessboardKit renders the chessboard based on the provided FEN string and updates it when the board position changes.
| FEN Rendering |
|---|
- ChessboardKit
- Use for your macOS or iOS/iPadOS app
- Table of Contents
- Features
- Supported Platforms
- Installation
- Quick Start
- Chess Engine
- Usage
- ChessboardModel
- Color Schemes
- Advanced Example Usage
- ❤️ Donations ❤️
- ❤️ Buy QuakeNotch to support me ❤️
- ❤️ Buy MacsyZones to support me ❤️
- Cryptocurrency Donations
- Contributing
- Acknowledgments
- License
- Render chessboards with customizable sizes and styles
- Support for FEN notation to set up board positions
- Interactive piece movement with legality checks
- Legal move highlighting
- Promotion picker for pawn promotions
- Slide animation for piece movements
- Highlighting squares for better user interaction
- Click/touch on source and destination squares
- Drag&drop support for piece movement
- Flip board functionality for player perspective
- Built-in FEN serialization and deserialization
- Fen validation
ChessboardKit is designed to work with the following platforms:
- iOS 17+
- macOS 14+
ChessboardKit is written in Swift 5 or later.
To use ChessboardKit in your project, add it as a dependency in your Package.swift:
.package(url: "https://github.com/rohanrhu/ChessboardKit.git", from: "1.0.0")Then, import the library in your Swift files:
import ChessboardKitAlso, you might want to use the ChessKit library:
import ChessKitNote
ChessKit will be automatically included when you add ChessboardKit to your project.
Here is a simple example to get started with ChessboardKit:
import SwiftUI
import ChessboardKit
struct ContentView: View {
@State var chessboardModel = ChessboardModel(fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
var body: some View {
Chessboard(chessboardModel: chessboardModel)
.onMove { move, isLegal, from, to, lan, promotionPiece in
print("Move: \(lan)")
}
.frame(width: 300, height: 300)
}
}ChessboardKit uses @aperechnev's amazing ChessKit chess engine. The ChessboardKit Swift package has it as dependency; when you add ChessboardKit to your project, it will automatically add ChessKit package to your project.
To set up a chessboard, initialize a ChessboardModel with a FEN string and pass it to the Chessboard view:
@State var chessboardModel = ChessboardModel(fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")Chessboard(chessboardModel: chessboardModel)You can handle moves using the onMove callback:
Note
The promotionPiece parameter will be nil if the move is not a promotion.
Chessboard(chessboardModel: chessboardModel)
.onMove { move, isLegal, from, to, lan, promotionPiece in
print("Move: \(lan)")
}Important
ChessboardKit doesn't perform the move on chess engine automatically. You need to handle the move logic in your app, such as updating the chess engine state and the chessboard model. Follow the example below to see how to handle moves.
When your onMove callback is called, you need to perform the move with the chess engine and update the chessboard model with the new FEN string:
.onMove { move, isLegal, from, to, lan, promotionPiece in
print("Move: Fen: \(chessboardModel.fen) - Lan: \(lan)")
if !isLegal {
print("Illegal move: \(lan)")
return
}
chessboardModel.game.make(move: move)
chessboardModel.setFen(FenSerialization.default.serialize(position: chessboardModel.game.position), lan: lan)
}Note
You can see what chessboardModel.game has for doing more.
You can customize the board size and appearance:
Chessboard(chessboardModel: chessboardModel)
.frame(width: 400, height: 400)The Chessboard view automatically adjusts its size based on the provided frame.
ChessboardKit renders the chessboard based on the provided FEN string. You can get the FEN string by ChessBoardModel.fen or set it by using the ChessboardModel.setFen(_ fen: String, lan: String? = nil) method.
chessboardModel.setFen("New FEN string")or directly set the FEN string:
chessboardModel.fen = "New FEN string"You can also get the current FEN string:
let currentFen = chessboardModel.fenWhen you use the setFen method by providing lan parameter too within SwiftUI's withAnimation, you can animate the chess movements.
withAnimation {
chessboardModel.setFen("New FEN string", lan: "Move LAN string")
}You can also use any animation with the withAnimation block.
withAnimation(.bouncy(duration: 1.)) {
chessboardModel.setFen("New FEN string", lan: "Move LAN string")
}You can pass the perspective parameter to your ChessboardModel to flip the board for the player's perspective:
@State var chessboardModel = ChessboardModel(fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", perspective: .white)You can highlight specific squares on the board for better user interaction:
chessboardModel.hint(row: 3, column: 4)To clear all highlighted squares:
chessboardModel.clearHint()ChessboardKit automatically highlights legal moves when a piece is selected or dragged. This feature is enabled by default and shows dots on squares where the selected piece can legally move.
To disable automatic legal move highlighting:
@State var chessboardModel = ChessboardModel(fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
highlightLegalMoves: false)
// ... or set ti here
chessboardModel.highlightLegalMoves = falseThe legal move dots are displayed using the legalMove color from your color scheme, which can be customized (see Color Schemes).
Indicate a waiting state on the board:
chessboardModel.beginWaiting()To end the waiting state:
chessboardModel.endWaiting()When it is time for a pawn promotion, the Chessboard view will automatically show a promotion picker and handle the promotion process. When a promotion is made, the onMove callback will receive the promotion piece as a parameter.
ChessboardKit has a FenValidation class that has a static method validateFen(_ fen: String) -> Bool to validate FEN strings. You can use this method to check if a FEN string is valid before using it in your chessboard.
let isValid = FenValidation.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")If you want Chessboard view to check the legality of moves, you can enable it with the validateMoves state of ChessboardModel.
@State var chessboardModel = ChessboardModel(validateMoves: true)ChessboardModel is the core data model that manages the state of the chessboard. It provides properties and methods to interact with the chessboard programmatically.
fen:String- The current FEN string representation of the board positionsize:CGFloat- The size of the chessboard in pointscolorScheme:ChessboardColorScheme- The color scheme used for the boardperspective:PieceColor- The side of the board (white or black) that appears at the bottomturn:PieceColor- The current player's turn (white or black)validateMoves:Bool- Whether to validate moves according to chess rulesallowOpponentMove:Bool- Whether to allow moves for the opponentinWaiting:Bool- Indicates if the board is in a waiting stateselectedSquare:BoardSquare?- The currently selected square on the boardhintedSquares:Set<BoardSquare>- A set of squares that are highlightedhighlightLegalMoves:Bool- Whether to highlight legal moves when a piece is selected or dragged (default:true)legalMoveSquares:Set<BoardSquare>- A set of squares that represent legal move destinationsshowPromotionPicker:Bool- Whether the promotion picker is currently displayedgame:Game- The underlying chess game objectcurrentMove:Move?andprevMove: Move?- Current and previous move objectspromotionPiece:Piece?- The piece being promoted when a promotion is in progressshouldFlipBoard:Bool- Whether the board should be flipped based on perspectivemovingPiece:(piece: Piece, from: BoardSquare, to: BoardSquare)?- Piece currently being animatedonMove:(Move, Bool, String, String, String, PieceKind?) -> Void- Callback for moves
ChessboardModel provides various methods to manipulate and interact with the chessboard.
Methods to set and manage the board position:
// Set a new FEN position with optional animation of opponent's move
func setFen(_ fen: String, lan: String? = nil)Methods to manage the selected squares:
// Deselect the currently selected square
func deselect()Methods to highlight squares for visual feedback:
// Highlight squares using various methods
func hint(_ square: BoardSquare)
func hint(_ square: String)
func hint(row: Int, column: Int)
func hint(_ squares: [BoardSquare])
func hint(_ squares: [String])
// Clear all highlighted squares
func clearHint()
// Highlight squares for a specific duration
func hint(_ square: String, for seconds: Double)
func hint(_ squares: [String], for seconds: Double)
func hint(_ squares: [BoardSquare], for seconds: Double)
func hint(_ square: BoardSquare, for seconds: Double)Methods to handle pawn promotion:
// Manage the promotion picker
func presentPromotionPicker(piece: Piece, sourceSquare: String, targetSquare: String, lan: String)
func absentePromotionPicker()
func togglePromotionPicker()
// Check if a move is a pawn promotion
func isPromotable(piece: Piece, lan: String) -> BoolMethods to control the board's interaction state:
// Control waiting state
func beginWaiting()
func endWaiting()You can initialize a ChessboardModel with custom options:
ChessboardModel(
fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", // FEN string
perspective: .white, // Board perspective
colorScheme: .light, // Color scheme
allowOpponentMove: false // Allow opponent moves
)ChessboardKit provides some default color schemes. Just pass the colorScheme parameter like ChessboardModel(colorScheme: .blue).
You can also make your own color schemes with the ChessboardColorScheme protocol. Here are some examples of default color schemes:
public protocol ChessboardColorScheme: Sendable {
var light: Color { get }
var dark: Color { get }
var label: Color { get }
var selected: Color { get }
var hinted: Color { get }
var legalMove: Color { get }
}public struct CustomColorScheme: ChessboardColorScheme {
public var light: Color = Color.white
public var dark: Color = Color.black
public var label: Color = Color.gray
public var selected: Color = Color.blue
public var hinted: Color = Color.red
public var legalMove: Color = Color(red: 0.30, green: 0.30, blue: 0.30, opacity: 0.4)
}Then, use it like this:
@State var chessboardModel = ChessboardModel(colorScheme: CustomColorScheme())Click to expand the full advanced example code
import SwiftUI
import ChessboardKit
import ChessKit
struct ContentView: View {
var body: some View {
VStack {
TestBoard()
}
.ignoresSafeArea()
.preferredColorScheme(.light)
}
}
public struct TestBoard: View {
static let POSITION = "5k2/1P2bn2/8/8/8/3Q4/3K4/8 w - - 0 1"
@State var showError: Bool = false
@State var errorMessage: String = ""
@State var size: CGFloat = 350
@Bindable var chessboardModel = ChessboardModel(fen: POSITION,
perspective: .white,
colorScheme: .light)
@State var fen = POSITION
var backgroundAnimationStartDate = Date()
public var body: some View {
VStack(spacing: 20) {
VStack {}.frame(height: 50)
Text("ChessboardKit Sample")
.font(.title)
.fontWeight(.bold)
Chessboard(chessboardModel: chessboardModel)
.onMove { move, isLegal, from, to, lan, promotionPiece in
print("Move: Fen: \(chessboardModel.fen) - Lan: \(lan)")
if !isLegal {
print("Illegal move: \(lan)")
return
}
chessboardModel.game.make(move: move)
chessboardModel.setFen(FenSerialization.default.serialize(position: chessboardModel.game.position), lan: lan)
}
.frame(width: size, height: size)
.padding(5)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.gray, lineWidth: 1))
VStack(alignment: .leading) {
Text("FEN Notation:")
.fontWeight(.medium)
TextField("Enter FEN", text: $fen)
.padding(10)
.background(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.gray, lineWidth: 1)
)
.onChange(of: fen) { _, newValue in
if !FenValidation.validateFen(newValue) {
showError = true
errorMessage = "Invalid FEN notation."
return
} else {
showError = false
errorMessage = ""
}
chessboardModel.setFen(newValue)
}
.onChange(of: chessboardModel.fen) {
fen = chessboardModel.fen
}
if showError {
Text(errorMessage)
.foregroundColor(.red)
.font(.caption)
}
}
Slider(value: $size, in: 200...350, step: 10) {
Text("Board Size: \(Int(chessboardModel.size))")
} minimumValueLabel: {
Text("200")
} maximumValueLabel: {
Text("350")
}
.padding(.horizontal)
Text("Board Size: \(Int(size))")
.font(.caption)
HStack {
Button {
print(FenSerialization.default.serialize(position: chessboardModel.game.position))
} label: {
Text("Print FEN")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
.buttonStyle(.plain)
Button {
withAnimation {
chessboardModel.fen = Self.POSITION
}
} label: {
Text("Reset")
.padding()
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(8)
.modifier {
if chessboardModel.fen == Self.POSITION {
$0.opacity(0.5)
} else { $0 }
}
}
.disabled(chessboardModel.fen == Self.POSITION)
Button {
chessboardModel.hint("d3", for: 1)
} label: {
Text("Hint")
.padding()
.background(Color.yellow)
.foregroundColor(.black)
.cornerRadius(8)
}
.buttonStyle(.plain)
}
.buttonStyle(.plain)
Button {
chessboardModel.togglePromotionPicker()
} label: {
VStack {
if chessboardModel.showPromotionPicker {
Text("Hide Promotion Picker")
} else {
Text("Show Promotion Picker")
}
}
.padding()
.background(chessboardModel.showPromotionPicker ? Color.red : Color.green)
.foregroundColor(.white)
.cornerRadius(8)
}
.buttonStyle(.plain)
Spacer()
}
.padding()
.background {
GeometryReader { proxy in
ZStack {
TimelineView(.animation) { context in
Color.white
.scaledToFill()
.visualEffect { content, proxy in
content
.colorEffect(ShaderLibrary.circlesBackground(
.boundingRect,
.float(backgroundAnimationStartDate.timeIntervalSinceNow),
.color(Color(hue: 0.0, saturation: 0.0, brightness: 0.935)),
.color(Color(hue: 0.0, saturation: 0.0, brightness: 0.890))
))
}
}
LinearGradient(
stops: [
.init(color: .white.opacity(0.1), location: 0),
.init(color: .white.opacity(0.9), location: 0.33)
],
startPoint: .top,
endPoint: .bottom
)
}
.frame(width: proxy.size.width, height: proxy.size.height)
.clipped()
}
}
}
}
#Preview {
ContentView()
}This example shows a complete implementation of a ChessboardKit sample with FEN validation, board size adjustment, hints, and a promotion picker.
You love ChessboardKit? You can support the development by making a donation. You have the following options to donate:
You can buy my QuakeNotch app to support me. QuakeNotch is a Quake Terminal and Apple Music Player that lives on your MacBook's notch.
You can buy my MacsyZones app to support me. MacsyZones is a revolutionary window manager for macOS.
| Currency | Address |
|---|---|
| ETH / USDT / USDC | 0x1D99B2a2D85C34d478dD8519792e82B18f861974 |
Preferably, donating USDT or USDC is recommended but you can donate any of the above currencies. 🥳
We welcome contributions to ChessboardKit. Please see the CONTRIBUTING.md file for more information.
Special thanks to @aperechnev of amazing ChessKit chess engine library for providing the chess engine used in this library.
Copyright (C) 2025, Oğuzhan Eroğlu rohanrhu2@gmail.com (https://meowingcat.io/)
ChessboardKit is licensed under the MIT License. See the LICENSE file for more information.