Skip to content

About

Split-flap departure-board text for SwiftUI and UIKit: every letter rolls, flips or flickers into place, played by Core Animation

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

SplitflapKit

Split-flap departure-board text for SwiftUI and UIKit — every letter rolls, flips or flickers into place, played by Core Animation

iOS, iPadOS, Mac Catalyst 15+ Swift 6.0+ license

A departures board with cities in their own scripts, the four transitions, numbers, emoji, cell widths and all four directions, all moving at once
Emoji: Twemoji, CC-BY 4.0

import SplitflapKit

Splitflap(gate)
    .splitflapTransition(.flip(surface: .black))
    .splitflapFont(size: 28, weight: .bold)

✨ Features

  • 🎰 Four transitions — flip, reel, roll and scramble, for words and for numbers
  • 🔤 Any text, any script — Latin, Cyrillic, Greek, Armenian, Georgian, the Indic and Southeast Asian scripts, the kana and many numeral systems roll through their own alphabets; any other character still changes, in one step. Flags, emoji and Indic syllables stay whole in one cell
  • 📏 Cells that keep still — tabular digits by default, and two cell widths: text-like natural and uniform, a station board's fixed cells
  • ⚡ Core Animation — every glyph is rasterised once into an atlas and every change is baked into keyframes, so the motion runs in the render server, not in your code
  • 🔁 Interruptible — a new text mid-flight continues from where every cell is, without a jump
  • ⏳ Loading built in — random words spin while data loads, then the real text lands
  • ♿ Accessible — one static-text element with the full text, Dynamic Type and Reduce Motion
  • 📐 Lays out like a label — intrinsic size of a one-line UILabel in the same font, … when narrower
  • 🧩 SwiftUI and UIKit — Splitflap and SplitflapView, iPhone, iPad and the Mac through Mac Catalyst, no dependencies

📦 Installation

Swift Package Manager — in Xcode, File → Add Package Dependencies… and paste:

https://github.com/enavermate/SplitflapKit

or in Package.swift:

.package(url: "https://github.com/enavermate/SplitflapKit", from: "0.5.3")

iOS and iPadOS 15 or later, and Mac Catalyst 15 or later.

🚀 Quick start

SwiftUI

struct Gate: View {
    @State private var status = "BOARDING"

    var body: some View {
        Splitflap(status)
            .splitflapTransition(.flip(surface: .black))
            .splitflapCellWidth(.uniform)
            .splitflapFont(size: 32, weight: .bold, design: .monospaced)
            .splitflapColor(.yellow)
    }
}

A new string turns in from the one on screen. Modifiers apply to every Splitflap below them, so a whole board takes one .splitflapTransition.

UIKit

let board = SplitflapView(text: "MADRID")
board.font = .systemFont(ofSize: 32, weight: .bold)
board.transition = .roll()
view.addSubview(board) // Auto Layout: it has an intrinsic size, like a label

board.text = "LISBOA"

🍳 Recipes

A departure board: one transition for the whole board

Modifiers flow down to every Splitflap inside, and the nearest one wins, so a column can still take its own colour.

struct Departures: View {
    let flights: [(time: String, city: String, status: String)]

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            ForEach(flights, id: \.city) { flight in
                HStack(spacing: 12) {
                    Splitflap(flight.time)
                    Splitflap(flight.city)
                    Splitflap(flight.status).splitflapColor(.yellow)
                }
            }
        }
        .splitflapTransition(.flip(surface: .black))
        .splitflapCellWidth(.uniform)
        .splitflapFont(size: 20, weight: .bold, design: .monospaced)
        .splitflapColor(.white)
    }
}

A price that holds still

Digits are tabular by default, so the price keeps its width; .auto rolls each digit the short way round.

struct Price: View {
    let amount: Decimal

    var body: some View {
        Splitflap(amount.formatted(.currency(code: "USD")))
            .splitflapTransition(.roll(direction: .auto))
            .splitflapFont(size: 34, weight: .semibold)
    }
}

A clock

struct Clock: View {
    var body: some View {
        TimelineView(.periodic(from: .now, by: 1)) { context in
            Splitflap(context.date.formatted(date: .omitted, time: .standard))
                .splitflapTransition(.reel(direction: .down))
                .splitflapCellWidth(.uniform)
        }
    }
}

Loading words instead of a skeleton

Random words spin while the data loads, then the real text lands. The range keeps the loading words near the size of what comes.

struct Greeting: View {
    let name: String?

    var body: some View {
        Splitflap(name ?? "")
            .splitflapLoading(name == nil, length: .range(min: 4, max: 10))
            .splitflapTransition(.flip(surface: .black))
    }
}

Hex codes, a custom alphabet and timing

struct Status: View {
    let code: String

    var body: some View {
        Splitflap(code)
            .splitflapTransition(.scramble)
            .splitflapAlphabet(.letters("0123456789ABCDEF"))
            .splitflapTiming(duration: 0.4, stagger: 0.02, order: .random)
    }
}

One script for every cell

By default each letter rolls through its own script's alphabet, read from the text; name a script to fix it.

struct City: View {
    let name: String

    var body: some View {
        Splitflap(name)
            .splitflapAlphabet(.script(.greek))
    }
}

Rows in a list

A reused row replans when its key changes, and shows its first text at once instead of animating in.

struct Row: View {
    let item: (id: Int, title: String)

    var body: some View {
        Splitflap(item.title)
            .splitflapContentKey(item.id)
            .splitflapAnimatesOnAppear(false)
    }
}

Knowing when a change starts and settles

struct Score: View {
    let score: Int
    @State private var settled = true

    var body: some View {
        Splitflap(String(score))
            .onSplitflapTransitionStart { _ in settled = false }
            .onSplitflapTransitionEnd { _, interrupted in
                if !interrupted { settled = true }
            }
            .opacity(settled ? 1 : 0.8)
    }
}

Motion, randomness and tracking

.never keeps the motion even with Reduce Motion on — for a decorative banner only. The same seed always plays the same way.

struct Banner: View {
    var body: some View {
        Splitflap("SALE")
            .splitflapReduceMotion(.never)
            .splitflapSeed(7)
            .splitflapLetterSpacing(2)
    }
}

UIKit: a view controller with loading

SplitflapView sizes itself like a one-line UILabel, so Auto Layout needs only a position.

final class GateViewController: UIViewController {
    private let board = SplitflapView(text: "")

    override func viewDidLoad() {
        super.viewDidLoad()
        board.font = .monospacedSystemFont(ofSize: 28, weight: .bold)
        board.textColor = .systemYellow
        board.transition = .flip(surface: .black)
        board.cellWidth = .uniform
        board.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(board)
        NSLayoutConstraint.activate([
            board.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            board.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])

        board.isLoading = true
        board.loadingLength = .cells(6)
        board.onTransitionEnd = { text, interrupted in
            print("settled on \(text)", interrupted ? "(interrupted)" : "")
        }
    }

    func show(gate: String) {
        board.isLoading = false
        board.text = gate
    }
}

The planner, without a view

SplitflapPlanner (re-exported by SplitflapKit) returns every cell's path and timing as plain data — for tests, or to draw the board some other way.

func plannedPaths() {
    let first = planTransition(createBoard(), TransitionInput(text: "MADRID", now: 0, instant: true))
    let next = planTransition(first.board, TransitionInput(text: "LISBOA", now: 1000, transition: .flip))
    for cell in next.plan.cells {
        print(cell.from, "→", cell.to, cell.path.joined(), "\(cell.delayMs) ms")
    }
}

🎞️ Transitions

Transition What every changed letter does
.reel() (default) Spins through the alphabet between the old letter and the new one, like a slot machine reel
.roll() Rolls through a few letters: its neighbours when the new one is close, random ones when it is far
.flip(surface:) Flips over in halves, like a departure board
.scramble Flickers through random letters of its own script, then locks

flip paints its flaps solid so the half they cover never shows through: surface is the colour right under the text, and the flaps vanish into it. reel and roll take a direction: .random (default — every other cell from above), .auto (each cell the way its letter lies in the alphabet), .up, .down.

Splitflap(weekday).splitflapTransition(.roll(direction: .auto))

📏 Cell width

SplitflapCellWidth Behaviour
.natural A cell moves from the old glyph's width to the new one's, as text reflows. reel and roll only
.uniform Every cell as wide as the alphabet's widest letter, each glyph centred — a station board

Without one, reel and roll reflow like text, and flip and scramble keep every letter close to its own width — narrow, regular and wide cells measured from the text's alphabets — since they show two glyphs in one cell at once and must not reflow.

⚙️ API

SwiftUI modifiers

Modifier What it sets
.splitflapTransition(_:) .reel(), .roll(), .flip(surface:), .scramble
.splitflapCellWidth(_:) .natural (reel, roll), .uniform; nil for the default
.splitflapTiming(duration:stagger:order:) seconds per cell (0.65), seconds between cells (0.04), .ltr .rtl .random
.splitflapAlphabet(_:) .auto, .script(.greek), .letters("0123456789ABCDEF")
.splitflapLoading(_:length:) random words until false; .cells(4) or .range(min: 4, max: 10)
.splitflapFont(_:), .splitflapFont(size:weight:design:) the glyphs' font, scaled with Dynamic Type
.splitflapColor(_:), .splitflapLetterSpacing(_:), .splitflapTabularDigits(_:) colour, tracking, digits of one width (on)
.splitflapSeed(_:) the same seed plays the same way
.splitflapContentKey(_:), .splitflapAnimatesOnAppear(_:) lists: replan a reused row, show the first text at once
.splitflapReduceMotion(_:) .system (default), .always, .never
.onSplitflapTransitionStart(_:), .onSplitflapTransitionEnd(_:) the text, and whether a newer text interrupted it

UIKit — SplitflapView has the same settings as properties: text, transition, cellWidth, duration, stagger, staggerOrder, alphabet, isLoading, loadingLength, seed, font, textColor, letterSpacing, lineHeight, usesTabularDigits, adjustsFontForContentSizeCategory, reduceMotion, animatesOnAppear, contentKey, onTransitionStart, onTransitionEnd.

The planner — createBoard(), planTransition(_:_:) and loadingPlan(_:seed:options:) return every cell's path, timing and direction as plain data, so a board's behaviour can be tested without rendering, or drawn some other way.

🔤 Scripts

A cell rolls through the alphabet of its own letter's script, read from the text — no language setting. A word travels only through the core letters and the extra letters it contains, so a Ukrainian word never flashes a letter only Belarusian uses. Chinese, Korean and anything else without an alphabet changes in one step: no script can break a board. Right-to-left scripts are not supported.

🌍 Also for React Native and the web

The same board, plan for plan: react-native-splitflap for React Native and Expo (npx expo install @enavermate/react-native-splitflap), and splitflap for the web — a <split-flap> element and a React component (npm install @enavermate/splitflap). SplitflapKit's planner is a line-by-line port checked against 131 scenarios of the TypeScript one, so a text animates the same way in all three. How each is built: scivi.dev/oss/splitflap.

🧪 Demo

Examples/SplitflapDemo — open SplitflapDemo.xcodeproj and run it on an iPhone, an iPad or, as a Mac Catalyst app, on the Mac.

🤝 Contributing

Found a bug, or missing something? Open an issue. See CONTRIBUTING; everyone taking part follows the code of conduct.

📄 License

MIT © Roman Diukachov

About

Split-flap departure-board text for SwiftUI and UIKit: every letter rolls, flips or flickers into place, played by Core Animation

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages