Stanza Redux is a cross-platform ebook reader for iOS and Android, built with Skip and powered by the Readium SDK. A single Swift codebase powers a platform-native app that utilizes SwiftUI on iOS and Jetpack Compose on Android, while each platform uses its own native Readium toolkit for EPUB parsing, rendering, and navigation.
Stanza Redux demonstrates how Skip can bridge platform-specific native libraries from a shared codebase. The app is organized into two Swift Package Manager modules:
Stanza— The UI layer (SwiftUI views), built inSources/Stanza/StanzaModel— The data layer (database, OPDS parsing, settings), built inSources/StanzaModel/
The Readium SDK is published as two independent toolkits:
- readium/swift-toolkit — Used on iOS via Swift Package Manager
- readium/kotlin-toolkit — Used on Android via Gradle dependencies
Stanza Redux uses #if SKIP / #if !SKIP conditional compilation to call the appropriate platform SDK while sharing all UI and business logic. Platform-specific types are abstracted behind thin wrappers defined in StanzaModel:
| Wrapper | iOS Type | Android Type |
|---|---|---|
Pub |
ReadiumShared.Publication |
org.readium.r2.shared.publication.Publication |
Loc |
ReadiumShared.Locator |
org.readium.r2.shared.publication.Locator |
Lnk |
ReadiumShared.Link |
org.readium.r2.shared.publication.Link |
Man |
ReadiumShared.Manifest |
org.readium.r2.shared.publication.Manifest |
These wrappers expose cross-platform properties (title, href, progression, etc.) that the UI layer consumes without needing to know which platform SDK is providing the data.
The EPUB navigator is embedded differently on each platform:
- iOS:
EPUBNavigatorViewControlleris wrapped in aUIViewControllerRepresentableand hosted in a SwiftUI view - Android:
EpubNavigatorFragmentis embedded via Jetpack Compose'sAndroidFragmentcomposable within aComposeView
StanzaApp (entry point)
└── ContentView (tab bar)
├── LibraryView (book management)
├── BrowseView (OPDS catalogs)
└── SettingsView (preferences)
└── AdvancedSettingsView
Shared state is managed through the SwiftUI environment:
StanzaSettings—@Observableclass persisting user preferences toUserDefaultsLibraryManager—@Observableclass managing the book database and file operationsErrorManager—@Observableclass providing centralized error alert presentation
The Library tab is the main screen of the app. It displays all imported books with cover art, titles, authors, and reading progress.
- Import books from the device's file system using the system document picker
- Cover art extraction — automatically extracts and caches cover images from EPUB files
- Reading progress — displays percentage complete for each book
- Search — filter the library by title or author
- Book management — long-press context menu to view details, edit metadata, or delete books
- Resume reading — tap a book to open it in the reader; the app remembers your last reading position
- Sample book — a bundled copy of Alice's Adventures in Wonderland can be imported to try the reader immediately
Each book has a detail view showing metadata (title, author, identifier), reading progress, chapter count, and file path. An edit mode allows modifying the title, author, and identifier.
The reader presents EPUB content in a paginated view with customizable typography and an overlay HUD for navigation controls.
- Tap zones — tap the left or right third of the screen to go backward or forward; tap the center to toggle the HUD
- Animated page turns — smooth page transition animations on both platforms
- Table of Contents — hierarchical chapter navigation with the current chapter highlighted
- Bookmarks — add, view, edit notes on, and navigate to bookmarks
- Reading position persistence — your position is saved on every page turn and restored on next launch
The heads-up display provides:
- Progress bar with chapter title and percentage
- Font size controls (increase/decrease buttons)
- Font picker — horizontal scrolling panel with font previews
- Spacing controls — cycle through presets for line height, character spacing, word spacing, and page margins
- Table of Contents and Bookmark buttons
- Light, Dark, and Sepia reading themes
- System appearance follows the device setting
- Status bar hiding for immersive reading (iOS)
The Catalogs tab (enabled via Advanced Settings) allows browsing and downloading books from OPDS catalog feeds.
- Pre-configured catalogs — Standard Ebooks, Project Gutenberg, and Ebooks Gratuits
- Custom catalogs — add any OPDS feed URL
- Catalog browsing — navigate categories, groups, and facets
- Search — search within catalogs that support OpenSearch
- Book detail — view cover art, author, summary, and available download formats
- Multiple format support — when a book is available in multiple EPUB variants (e.g., "Recommended compatible epub", "Advanced epub"), a menu lets you choose which to download
- Direct download — download and import books directly into the library
- About This Catalog — displays feed metadata including icon, description, total book count, and informational links
The OPDS service (OPDSService) handles:
- Parsing both OPDS 1 (Atom/XML) and OPDS 2 (JSON) feeds via the Readium OPDS parsers
- Extracting navigation images from feed entries using raw XML parsing with SkipXML
- Promoting Atom
rel="enclosure"entries to proper publication entries for feeds that don't use standard OPDS acquisition links - OpenSearch template resolution for catalog search
- HTML-to-Markdown conversion of book summaries using
HTMLMarkdown
The Settings tab provides controls for reading preferences, text layout, and spacing.
- Appearance — System, Light, or Dark mode
- Sepia Theme — warm-toned reading theme
- Font — choose from system fonts and bundled custom fonts (Montserrat, Noto Serif, Noto Sans)
- Font Size — adjustable from 50% to 300%
- Animate Page Turns — toggle animated transitions
- Left Tap Advances — swap the left-tap direction
- Hide Status Bar in Reader — immersive reading mode
- Open Web Pages in Embedded Browser — use SFSafariViewController (iOS) or Chrome Custom Tabs (Android)
- Columns (auto, one, two)
- Content fit (auto, page, width)
- Hyphenation, text alignment, text normalization
- Publisher styles toggle
- Line height, page margins, paragraph spacing, word spacing (all with slider controls)
- Enable Catalogs — show/hide the Catalogs tab
The app uses a centralized ErrorManager that provides consistent error alerts across all screens. When an error occurs anywhere in the app, errorManager.errorOccurred(info:) is called with structured ErrorInfo containing a title, message, underlying error, and optional help URL. The error manager logs the error and presents an alert with "OK" to dismiss and "Help" to search the project's issue tracker.
The ErrorManager is safe to call from any thread — it dispatches to the main actor internally.
This project is both a stand-alone Swift Package Manager module, as well as an Xcode project that builds and transpiles the project into a Kotlin Gradle project for Android using the Skip plugin.
Building the module requires that Skip be installed using
Homebrew with brew install skiptools/skip/skip.
This will also install the necessary transpiler prerequisites: Kotlin, Gradle, and the Android build tools.
Installation prerequisites can be confirmed by running skip checkup.
The module can be tested using the standard swift test command
or by running the test target for the macOS destination in Xcode,
which will run the Swift tests as well as the transpiled
Kotlin JUnit tests in the Robolectric Android simulation environment.
Parity testing can be performed with skip test,
which will output a table of the test results for both platforms.
Xcode and Android Studio must be downloaded and installed in order to run the app in the iOS simulator / Android emulator. An Android emulator must already be running, which can be launched from Android Studio's Device Manager.
To run both the Swift and Kotlin apps simultaneously, launch the StanzaApp target from Xcode. A build phases runs the "Launch Android APK" script that will deploy the transpiled app a running Android emulator or connected device. Logging output for the iOS app can be viewed in the Xcode console, and in Android Studio's logcat tab for the transpiled Kotlin app.
This software is licensed under the GNU General Public License v2.0 or later.