A SwiftUI‑first component for rendering EPUB books.
EPUBReaderSwiftUI enables you to display .epub files within a SwiftUI view or container.
It supports the essential EPUB workflow: file selection, unpacking, and rendering of chapters/HTML content, while using a SwiftUI‑friendly interface.
- Drop-in SwiftUI view (
EPUBReaderView) — works with.sheet,.fullScreenCover, or inline. - Local & remote EPUB loading — pass a file URL, a remote URL string, or an
EPUBSourceenum. Remote files are downloaded and cached automatically. - Customizable overlay — replace the default reader chrome (toolbar, progress bar, font controls) with your own SwiftUI view via an
overlay:closure. - Customizable table of contents — supply a
tocView:closure to build your own TOC UI, or use the built-inDefaultEPUBReaderTOCView. - Highlighting & notes — select text to highlight, tap highlights to edit color/style, add notes, or delete. Built-in default UI or fully custom via callbacks.
- Bookmarks — toggle bookmarks from the overlay; the default overlay includes a bookmark button out of the box. Your app owns the array and persists it.
- Reading position persistence — save and restore the locator across sessions.
- Font-size control via
EPUBReaderSwiftUIPreferences. - Navigation across chapters, progress tracking.
- Renders content via WKWebView (Readium) inside SwiftUI.
- No need to
import ReadiumShared— common types (Locator,Link) are re-exported.
Add EPUBReaderSwiftUI as a Swift Package in Xcode:
File → Add Packages… → `https://github.com/jtCodes/EPUBReaderSwiftUI.git`
Alternatively, integrate manually by copying the Sources/EPUBReaderSwiftUI folder.
Note:
ReadiumSharedis re-exported automatically — you only needimport EPUBReaderSwiftUI.
Pass a URL string directly. The package downloads, caches, and opens the file for you.
import SwiftUI
import EPUBReaderSwiftUI
struct RemoteURLExample: View {
@State private var showReader = false
@State private var savedLocator: EPUBReaderSwiftUILocator?
@State private var savedPreferences = EPUBReaderSwiftUIPreferences()
var body: some View {
Button("Open Remote EPUB") {
showReader = true
}
.fullScreenCover(isPresented: $showReader) {
EPUBReaderView(
remoteURL: "https://www.gutenberg.org/ebooks/9662.epub3.images",
initialLocator: savedLocator,
initialPreferences: savedPreferences
) { locator, preferences in
savedLocator = locator
savedPreferences = preferences
showReader = false
}
}
}
}Load an .epub that's already on disk or in your app bundle.
struct LocalFileExample: View {
@State private var showReader = false
@State private var savedLocator: EPUBReaderSwiftUILocator?
@State private var savedPreferences = EPUBReaderSwiftUIPreferences()
var body: some View {
Button("Open Local EPUB") {
showReader = true
}
.fullScreenCover(isPresented: $showReader) {
if let url = Bundle.main.url(forResource: "republic", withExtension: "epub") {
EPUBReaderView(
url: url,
initialLocator: savedLocator,
initialPreferences: savedPreferences
) { locator, preferences in
savedLocator = locator
savedPreferences = preferences
showReader = false
}
}
}
}
}Use the EPUBSource enum when you need to decide between local and remote at runtime.
struct SourceEnumExample: View {
@State private var showReader = false
@State private var savedLocator: EPUBReaderSwiftUILocator?
@State private var savedPreferences = EPUBReaderSwiftUIPreferences()
let source: EPUBSource = .remoteURL(
"https://www.gutenberg.org/ebooks/9662.epub3.images",
useCache: true
)
// Or: let source: EPUBSource = .fileURL(Bundle.main.url(forResource: "republic", withExtension: "epub")!)
var body: some View {
Button("Open EPUB") {
showReader = true
}
.fullScreenCover(isPresented: $showReader) {
EPUBReaderView(
source: source,
initialLocator: savedLocator,
initialPreferences: savedPreferences
) { locator, preferences in
savedLocator = locator
savedPreferences = preferences
showReader = false
}
}
}
}Supply an overlay closure to replace the default reader chrome. You receive an EPUBReaderOverlayContext with all the state and actions you need (title, author, current locator, font-size binding, close/navigate callbacks, etc.).
struct CustomOverlayExample: View {
@State private var showReader = false
@State private var savedLocator: EPUBReaderSwiftUILocator?
@State private var savedPreferences = EPUBReaderSwiftUIPreferences()
var body: some View {
Button("Open with Custom Overlay") {
showReader = true
}
.fullScreenCover(isPresented: $showReader) {
EPUBReaderView(
remoteURL: "https://www.gutenberg.org/ebooks/9662.epub3.images",
initialLocator: savedLocator,
initialPreferences: savedPreferences
) { locator, preferences in
savedLocator = locator
savedPreferences = preferences
showReader = false
} overlay: { context in
// Build any SwiftUI overlay you want.
// 'context' exposes: .title, .author, .chapterTitle,
// .totalProgression, .showControls, .fontSize,
// .close(), .showTableOfContents(), .goToProgression(_:)
VStack {
if context.showControls {
HStack {
Button(action: context.close) {
Image(systemName: "chevron.left")
Text("Library")
}
Spacer()
Button(action: context.showTableOfContents) {
Image(systemName: "list.bullet")
}
}
.padding()
.background(.ultraThinMaterial)
}
Spacer()
}
}
}
}
}Supply a tocView closure to replace the default TOC sheet. You receive an EPUBReaderTOCContext with the chapter list, current locator, and navigation/dismiss actions.
struct CustomTOCExample: View {
@State private var showReader = false
@State private var savedLocator: EPUBReaderSwiftUILocator?
@State private var savedPreferences = EPUBReaderSwiftUIPreferences()
var body: some View {
Button("Open with Custom TOC") {
showReader = true
}
.fullScreenCover(isPresented: $showReader) {
EPUBReaderView(
remoteURL: "https://www.gutenberg.org/ebooks/9662.epub3.images",
initialLocator: savedLocator,
initialPreferences: savedPreferences
) { locator, preferences in
savedLocator = locator
savedPreferences = preferences
showReader = false
} overlay: { context in
// Keep the default overlay, only customize the TOC
DefaultEPUBReaderOverlay(context: context)
} tocView: { context in
// 'context' exposes: .tableOfContents, .currentLocator,
// .navigateToLink(_:), .dismiss()
NavigationView {
List(context.tableOfContents, id: \.href) { link in
Button {
context.navigateToLink(link)
} label: {
Text(link.title ?? "Untitled")
}
}
.navigationTitle("Chapters")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Close") { context.dismiss() }
}
}
}
}
}
}
}Tip: You can customize just the overlay, just the TOC, or both. Omit either closure to keep the default.
EPUBReaderSwiftUI has built-in highlighting support powered by Readium's Decoration API. Users can select text, tap Highlight in the context menu, and then tap existing highlights to change their color, style, add a note, or delete them.
Highlighting is opt-in — the "Highlight" menu item only appears when you provide an onHighlightCreated callback. Your app owns the [EPUBHighlight] array and is responsible for persisting it (UserDefaults, Core Data, CloudKit, etc.).
Pass highlights and onHighlightCreated — the library provides a default popover for editing color, style, notes, and deleting:
struct HighlightExample: View {
@State private var showReader = false
@State private var savedLocator: EPUBReaderSwiftUILocator?
@State private var savedPreferences = EPUBReaderSwiftUIPreferences()
@State private var highlights: [EPUBHighlight] = []
var body: some View {
Button("Open Reader") { showReader = true }
.fullScreenCover(isPresented: $showReader) {
EPUBReaderView(
remoteURL: "https://www.gutenberg.org/ebooks/1497.epub3.images",
initialLocator: savedLocator,
initialPreferences: savedPreferences,
highlights: $highlights,
onHighlightCreated: { highlights.append($0) },
onClose: { locator, preferences in
savedLocator = locator
savedPreferences = preferences
showReader = false
}
)
}
}
}To replace the built-in edit UI with your own, provide an onHighlightTapped callback:
EPUBReaderView(
remoteURL: "https://example.com/book.epub",
highlights: $highlights,
onHighlightCreated: { highlights.append($0) },
onHighlightTapped: { event in
// event.highlight — the tapped EPUBHighlight
// event.rect — bounding rect for anchoring a popover
selectedHighlight = event.highlight
showMyCustomEditor = true
},
onClose: { locator, preferences in
showReader = false
}
)| Property | Type | Description |
|---|---|---|
id |
String |
Unique identifier (UUID by default). |
locator |
Locator |
Readium locator — position in the book. |
color |
EPUBHighlightColor |
.yellow, .green, .blue, .red, .purple. |
style |
EPUBHighlightStyle |
.highlight (background) or .underline. |
note |
String? |
Optional user note attached to the highlight. |
highlightText |
String? |
The selected text (read-only, from the locator). |
EPUBHighlight conforms to Codable, Identifiable, Equatable, and Hashable, so you can serialize it directly with JSONEncoder/JSONDecoder.
Pass a bookmarks binding and the default overlay will show a bookmark toggle button (filled when the current chapter is bookmarked). No extra callbacks needed — the library handles add/remove automatically via EPUBReaderOverlayContext.toggleBookmark.
struct BookmarkExample: View {
@State private var showReader = false
@State private var savedLocator: EPUBReaderSwiftUILocator?
@State private var savedPreferences = EPUBReaderSwiftUIPreferences()
@State private var bookmarks: [EPUBBookmark] = []
var body: some View {
Button("Open Reader") { showReader = true }
.fullScreenCover(isPresented: $showReader) {
EPUBReaderView(
remoteURL: "https://www.gutenberg.org/ebooks/1497.epub3.images",
initialLocator: savedLocator,
initialPreferences: savedPreferences,
bookmarks: $bookmarks,
onClose: { locator, preferences in
savedLocator = locator
savedPreferences = preferences
showReader = false
}
)
}
}
}In custom overlays, use the bookmark properties on EPUBReaderOverlayContext:
// Inside your custom overlay:
Button(action: context.toggleBookmark) {
Image(systemName: context.isCurrentPageBookmarked ? "bookmark.fill" : "bookmark")
}
// List all bookmarks:
ForEach(context.bookmarks) { bookmark in
Text(bookmark.chapterTitle ?? "Unknown")
}| Property | Type | Description |
|---|---|---|
id |
String |
Unique identifier (UUID by default). |
locator |
Locator |
Readium locator — position in the book. |
createdAt |
Date |
When the bookmark was created. |
chapterTitle |
String? |
Chapter title (read-only, from the locator). |
progression |
Double? |
Overall reading progression 0.0–1.0 (read-only). |
EPUBBookmark conforms to Codable, Identifiable, Equatable, and Hashable.
- The EPUB file is unzipped into a temporary directory.
- The OPF file (manifest/spine) is parsed to discover chapters, metadata and Table of Contents.
- A
WKWebViewinside SwiftUI is used to display the HTML content of each chapter. - Navigation controls allow moving between chapters or pages.
- State management handles current chapter, progress, bookmarks.
- Standard EPUB 2/EPUB 3 packages (ZIP‑based).
- Basic HTML + CSS rendering via WebKit.
- Table of Contents.
- Local images/fonts inside the EPUB.
- Does not support advanced EPUB features such as media overlays (audio syncing), fixed‑layout books, or interactive content (unless explicitly added).
- Performance may degrade for very large books or very high font sizes — advisable to test on real devices.
- Annotations beyond highlights and bookmarks are not included out-of-the-box (you may need to extend).
- SwiftUI integration means there may be some bridging to UIKit/WebKit under the hood.
MIT (or whichever license specified in the repository).
See LICENSE for details.
Contributions, bug reports and feature requests are welcome!
Please open an issue or PR describing the improvement, and follow the code style (SwiftLint, SwiftFormat) as used in the project.
Created by jtCodes — thank you for sharing your work.
Enjoy reading!