Contents

rohanrhu/chessboardkit

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.

Use for your macOS or iOS/iPadOS app

Move with drag&drop or click/touch

| iOS | macOS | |------------------------------------------|------------------------------------------| | [alt text] | [alt text] |

Chess Features

| Promotion Picker | Suits its container's size | |--------------------------------------------|------------------------------------------| | [iOS Promotion] | [iOS Sizing] |

| Hint | Move Legality Check | |----------------------------------------|----------------------------------------| | [iOS Hint] | [iOS Move] |

Render the chessboard with only FEN string

ChessboardKit renders the chessboard based on the provided FEN string and updates it when the board position changes.

| FEN Rendering | |-------------------------------------------| | [macOS FEN] |

Table of Contents

- Use for your macOS or iOS/iPadOS app - Move with drag\&drop or click/touch - Chess Features - Render the chessboard with only FEN string - Table of Contents - Features - Supported Platforms - Swift Version - Installation - Quick Start - Chess Engine - Usage - Setting Up the Chessboard - Handling Moves - Making Moves - Board Size - Customizing the Chessboard - Advanced Features - FEN Rendering - Sliding Animation for Piece Movements - Flip The Board for Player Perspective - Highlighting Squares - Legal Move Highlighting - Waiting State and Avoiding User Interaction - Pawn Promotion - Fen Validation - Move Legality Check - ChessboardModel - Properties - Methods - Board Position Management - Square Selection - Square Highlighting - Promotion Management - Interaction State - Initialization - Color Schemes - Custom Color Scheme Example - Advanced Example Usage - ❤️ Donations ❤️ - ❤️ Buy QuakeNotch to support me ❤️ - ❤️ Buy MacsyZones to support me ❤️ - Cryptocurrency Donations - Contributing - Acknowledgments - License

Features

  • 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

Supported Platforms

ChessboardKit is designed to work with the following platforms:

  • iOS 17+
  • macOS 14+

Swift Version

ChessboardKit is written in Swift 5 or later.

Installation

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 ChessboardKit

Also, you might want to use the ChessKit library:

import ChessKit

[!NOTE] ChessKit will be automatically included when you add ChessboardKit to your project.

Quick Start

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)
    }
}

Chess Engine

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.

Usage

Setting Up the Chessboard

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)

Handling Moves

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.

Making 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.

Board Size

You can customize the board size and appearance:

Chessboard(chessboardModel: chessboardModel)
    .frame(width: 400, height: 400)

Customizing the Chessboard

The Chessboard view automatically adjusts its size based on the provided frame.

Advanced Features

FEN Rendering

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.fen
Sliding Animation for Piece Movements

When 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")
}
Flip The Board for Player Perspective

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)
Highlighting Squares

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()
Legal Move Highlighting

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 = false

The legal move dots are displayed using the legalMove color from your color scheme, which can be customized (see Color Schemes).

Waiting State and Avoiding User Interaction

Indicate a waiting state on the board:

chessboardModel.beginWaiting()

To end the waiting state:

chessboardModel.endWaiting()
Pawn Promotion

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.

Fen Validation

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")
Move Legality Check

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

ChessboardModel is the core data model that manages the state of the chessboard. It provides properties and methods to interact with the chessboard programmatically.

Properties

  • fen: String - The current FEN string representation of the board position
  • size: CGFloat - The size of the chessboard in points
  • colorScheme: ChessboardColorScheme - The color scheme used for the board
  • perspective: PieceColor - The side of the board (white or black) that appears at the bottom
  • turn: PieceColor - The current player's turn (white or black)
  • validateMoves: Bool - Whether to validate moves according to chess rules
  • allowOpponentMove: Bool - Whether to allow moves for the opponent
  • inWaiting: Bool - Indicates if the board is in a waiting state
  • selectedSquare: BoardSquare? - The currently selected square on the board
  • hintedSquares: Set<BoardSquare> - A set of squares that are highlighted
  • highlightLegalMoves: 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 destinations
  • showPromotionPicker: Bool - Whether the promotion picker is currently displayed
  • game: Game - The underlying chess game object
  • currentMove: Move? and prevMove: Move? - Current and previous move objects
  • promotionPiece: Piece? - The piece being promoted when a promotion is in progress
  • shouldFlipBoard: Bool - Whether the board should be flipped based on perspective
  • movingPiece: (piece: Piece, from: BoardSquare, to: BoardSquare)? - Piece currently being animated
  • onMove: (Move, Bool, String, String, String, PieceKind?) -> Void - Callback for moves

Methods

ChessboardModel provides various methods to manipulate and interact with the chessboard.

Board Position Management

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)
Square Selection

Methods to manage the selected squares:

// Deselect the currently selected square
func deselect()
Square Highlighting

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)
Promotion Management

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) -> Bool
Interaction State

Methods to control the board's interaction state:

// Control waiting state
func beginWaiting()
func endWaiting()

Initialization

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
)

Color Schemes

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 }
}

Custom Color Scheme Example

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())

Advanced Example Usage

<details>
<summary>Click to expand the full advanced example code</summary>

```swift
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()
}
```

</details>

This example shows a complete implementation of a ChessboardKit sample with FEN validation, board size adjustment, hints, and a promotion picker.

❤️ Donations ❤️

You love ChessboardKit? You can support the development by making a donation. You have the following options to donate:

❤️ Buy QuakeNotch to support me ❤️

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.

❤️ Buy MacsyZones to support me ❤️

You can buy my MacsyZones app to support me. MacsyZones is a revolutionary window manager for macOS.

Cryptocurrency Donations

| Currency | Address | | ----------------- | ----------------------------------------------------------------------------------------------- | | ETH / USDT / USDC | 0x1D99B2a2D85C34d478dD8519792e82B18f861974 |

Preferably, donating USDT or USDC is recommended but you can donate any of the above currencies. 🥳

Contributing

We welcome contributions to ChessboardKit. Please see the CONTRIBUTING.md file for more information.

Acknowledgments

Special thanks to @aperechnev of amazing ChessKit chess engine library for providing the chess engine used in this library.

License

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.

Package Metadata

Repository: rohanrhu/chessboardkit

Default branch: main

README: README.md