### Displaying Accessibility Metadata with W3C Guide Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Accessibility.md Initialize and use the AccessibilityMetadataDisplayGuide to interpret and present accessibility metadata. This example shows how to check for visual adjustments. ```swift let guide = AccessibilityMetadataDisplayGuide(publication: publication) switch guide.waysOfReading.visualAdjustments { case .modifiable: // The text and layout of the publication can be customized. case .unmodifiable: // The text and layout cannot be modified. case .unknown: // No metadata provided } ``` -------------------------------- ### SwiftUI LCP Authentication Setup Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Readium LCP.md Initialize `LCPObservableAuthentication` as a `@StateObject` and present `LCPDialog` when an authentication request is made. This setup is necessary for SwiftUI applications. ```swift @main struct MyApp: App { private let lcpService: LCPService private let publicationOpener: PublicationOpener @StateObject private var lcpAuthentication: LCPObservableAuthentication init() { // Create an `LCPObservableAuthentication` which will be used // to initialize the `LCPContentProtection`. // // With SwiftUI, it must be stored in a `@StateObject` property // to observe the authentication requests. let lcpAuthentication = LCPObservableAuthentication() _lcpAuthentication = StateObject(wrappedValue: lcpAuthentication) lcpService = LCPService(...) publicationOpener = PublicationOpener( ..., contentProtections: [ lcpService.contentProtection(with: lcpAuthentication) ] ) } var body: some Scene { WindowGroup { ContentView() // You can present an `LCPDialog` when the `LCPObservableAuthentication` // `request` property is updated. .sheet(item: $lcpAuthentication.request) { LCPDialog(request: $0) } } } } } ``` -------------------------------- ### Example Enum Picker Usage Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Preferences.md This is an example of how to use the `pickerRow` view for an enum preference, providing a specific formatting closure for the enum values. ```swift pickerRow( title: "Reading progression", preference: readingProgression, commit: commit, formatValue: { v in switch v { case .ltr: return "LTR" case .rtl: return "RTL" } } ) ``` -------------------------------- ### Start TTS Playback Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/TTS.md Begin reading the publication aloud from the beginning using the initialized PublicationSpeechSynthesizer. ```swift synthesizer.start() ``` -------------------------------- ### Displaying All Recommended Accessibility Fields Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Accessibility.md Iterate over all accessibility fields in the guide, skipping fields that should not be displayed. Print the localized title and statements for each field. ```swift for field in guide.fields { guard field.shouldDisplay else { continue } print("Section: \(field.localizedTitle)") for statement in field.statements { print(statement.localizedString(descriptive: false)) } } ``` -------------------------------- ### Start TTS from Visible Page Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/TTS.md Initiates TTS playback from the currently visible page in a VisualNavigator. Requires obtaining the first visible element's locator. ```swift if let start = await navigator.firstVisibleElementLocator() { synthesizer.start(from: start) } ``` -------------------------------- ### Get LCP License from Opened Publication Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Readium LCP.md Access the `LCPLicense` object directly from an already opened `Publication` using the `lcpLicense` property. ```swift let lcpLicense = publication.lcpLicense ``` -------------------------------- ### Setup DirectionalNavigationAdapter and Observers Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Bind the DirectionalNavigationAdapter to the navigator before adding custom observers. Return `true` from observers to stop event propagation. ```swift /// This adapter will automatically turn pages when the user taps the /// screen edges or press arrow keys. /// /// Bind it to the navigator before adding your own observers to prevent /// triggering your actions when turning pages. DirectionalNavigationAdapter().bind(to: navigator) // Clear the current search highlight on tap. navigator.addObserver(.tap { [weak self] _ in guard let searchViewModel = self?.searchViewModel, searchViewModel.selectedLocator != nil else { return false } searchViewModel.selectedLocator = nil return true }) // Toggle the navigation bar on tap, if nothing else took precedence. navigator.addObserver(.tap { [weak self] _ in self?.toggleNavigationBar() return true }) ``` -------------------------------- ### Get Publication Content from Locator Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Content.md Request the publication's content starting from a specific locator. If the locator is missing, content extraction begins from the start of the publication. ```swift guard let content = publication.content(from: startLocator) else { // Abort as the content cannot be extracted return } ``` -------------------------------- ### Initialize LCPService with Adapters Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Readium LCP.md Set up the LCPService by providing necessary adapters for LCP client, repositories, and asset retrieval. Uses Keychain for secure storage. ```swift import R2LCPClient import ReadiumLCP let httpClient = DefaultHTTPClient() let assetRetriever = AssetRetriever( httpClient: httpClient ) let lcpService = LCPService( client: LCPClientAdapter(), licenseRepository: LCPKeychainLicenseRepository(), passphraseRepository: LCPKeychainPassphraseRepository(), assetRetriever: assetRetriever, httpClient: httpClient ) /// Facade to the private R2LCPClient.framework. class LCPClientAdapter: ReadiumLCP.LCPClient { func createContext(jsonLicense: String, hashedPassphrase: LCPPassphraseHash, pemCrl: String) throws -> LCPClientContext { try R2LCPClient.createContext(jsonLicense: jsonLicense, hashedPassphrase: hashedPassphrase, pemCrl: pemCrl) } func decrypt(data: Data, using context: LCPClientContext) -> Data? { R2LCPClient.decrypt(data: data, using: context as! DRMContext) } func findOneValidPassphrase(jsonLicense: String, hashedPassphrases: [LCPPassphraseHash]) -> LCPPassphraseHash? { R2LCPClient.findOneValidPassphrase(jsonLicense: jsonLicense, hashedPassphrases: hashedPassphrases) } } ``` -------------------------------- ### Initialize EPUB Navigator with GCDHTTPServer Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Instantiate an EPUB navigator using an existing GCDHTTPServer instance. The navigator will manage publication registration automatically. ```swift import ReadiumNavigator import ReadiumAdapterGCDWebServer let navigator = try EPUBNavigatorViewController( publication: publication, httpServer: GCDHTTPServer.shared ) ``` -------------------------------- ### Initialize PublicationOpener with LCP Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Readium LCP.md Initialize PublicationOpener with LCP content protection. Requires an LCPService and an authentication implementation. ```swift let httpClient = DefaultHTTPClient() let authentication = LCPDialogAuthentication() let publicationOpener = PublicationOpener( parser: DefaultPublicationParser( httpClient: httpClient, assetRetriever: AssetRetriever(httpClient: httpClient), pdfFactory: DefaultPDFDocumentFactory() ), contentProtections: [ lcpService.contentProtection(with: authentication) ] ) ``` -------------------------------- ### Instantiate EPUB Navigator Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Navigator.md Use `EPUBNavigatorViewController` to present EPUB publications. Ensure the publication conforms to the EPUB profile before instantiation. ```swift if publication.conforms(to: .epub) { let navigator = try EPUBNavigatorViewController( publication: publication, initialLocation: lastReadLocation ) hostViewController.present(navigator, animated: true) } ``` -------------------------------- ### Update CocoaPods for ReadiumGCDWebServer Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Replace the old GCDWebServer pod with ReadiumGCDWebServer in your Podfile before running pod install. ```ruby pod 'ReadiumGCDWebServer', podspec: 'https://raw.githubusercontent.com/readium/GCDWebServer/4.0.0/GCDWebServer.podspec' ``` -------------------------------- ### Initialize TestApp with Specific Toolkit Version Source: https://github.com/readium/swift-toolkit/blob/develop/MAINTAINING.md Initialize the TestApp with a specified version of the toolkit using `make spm`. ```shell make spm version=VERSION lcp=... ``` -------------------------------- ### Constructing a PublicationOpener Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Open Publication.md Initialize a PublicationOpener with a PublicationParser and optional content protections for DRM. ```swift let publicationOpener = PublicationOpener( parser: DefaultPublicationParser( httpClient: httpClient, assetRetriever: assetRetriever, pdfFactory: DefaultPDFDocumentFactory() ), contentProtections: [ lcpService.contentProtection(with: LCPDialogAuthentication()) ] ) ``` -------------------------------- ### Add ReadiumInternal to CocoaPods Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Add the ReadiumInternal pod to your Podfile and run pod install to include the new internal utilities package. ```ruby pod 'ReadiumInternal', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/2.5.0/Support/CocoaPods/ReadiumInternal.podspec' ``` -------------------------------- ### Instantiate Audio Navigator Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Navigator.md Create an `AudioNavigator` for audiobook publications. This navigator is chromeless, allowing for custom UI development. ```swift let navigator = AudioNavigator( publication: publication, initialLocation: lastReadLocation ) navigator.play() ``` -------------------------------- ### Set Initial Navigator Preferences and Defaults Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Preferences.md When opening a publication, you can immediately apply user preferences and default settings to the Navigator. This is done by providing `preferences` and `defaults` to the Navigator's configuration during initialization. ```swift let navigator = try EPUBNavigatorViewController( publication: publication, config: .init( preferences: EPUBPreferences( language: Language(code: "fr") ), defaults: EPUBDefaults( pageMargins: 1.5, scroll: true ) ) ) ``` -------------------------------- ### Build with Readium LCP using Swift Package Manager Source: https://github.com/readium/swift-toolkit/blob/develop/TestApp/README.md Build the project with Readium LCP support by providing the LCP client framework URL to the 'make' command. This requires obtaining the framework from EDRLab. ```sh make spm lcp=https://.../Package.swift ``` -------------------------------- ### Prepare Release Script Source: https://github.com/readium/swift-toolkit/blob/develop/MAINTAINING.md Execute the release preparation script, which automates several release-related tasks. ```shell scripts/release-prepare.sh VERSION ``` -------------------------------- ### Extract Whole Raw Text Content Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Content.md A helper is available on Content to get the entire raw text of a publication. This operation can be expensive, so consider caching the result if it will be reused. ```swift let wholeText = content.text() ``` -------------------------------- ### Migrate Existing LCP Data from SQLite to Keychain Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Run this migration code once, for example, during an app update, to transfer existing LCP data from SQLite databases to the more secure Keychain. ```swift import ReadiumAdapterLCPSQLite import ReadiumLCP let keychainLicenseRepository = LCPKeychainLicenseRepository() let keychainPassphraseRepository = LCPKeychainPassphraseRepository() let sqliteLicenseRepository = try LCPSQLiteLicenseRepository() let sqlitePassphraseRepository = try LCPSQLitePassphraseRepository() try await sqliteLicenseRepository.migrate(to: keychainLicenseRepository) try await sqlitePassphraseRepository.migrate(to: keychainPassphraseRepository) ``` -------------------------------- ### Create GitHub Release Script Source: https://github.com/readium/swift-toolkit/blob/develop/MAINTAINING.md Create a draft release on GitHub, pre-filled with documentation links and changelog. ```shell scripts/release-github.sh ``` -------------------------------- ### Initialize PublicationSpeechSynthesizer Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/TTS.md Create an instance of PublicationSpeechSynthesizer to manage TTS playback for a publication. You can specify a default language for the synthesizer. ```swift let synthesizer = PublicationSpeechSynthesizer( publication: publication, config: PublicationSpeechSynthesizer.Configuration( defaultLanguage: Language("fr") ) ) ``` -------------------------------- ### Extract Publication Cover Fitting Max Size Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Use `publication.coverFitting(maxSize:)` to get the best resolution cover image without exceeding a specified size, useful for caching and avoiding large image downloads. ```swift let cover = publication.coverFitting(maxSize: CGSize(width: 100, height: 100)) ``` -------------------------------- ### Initialize LCPService with SQLite Adapter Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Use this snippet to initialize the LCPService with the new ReadiumAdapterLCPSQLite package. Ensure you have updated your project dependencies. ```swift import ReadiumAdapterLCPSQLite import ReadiumLCP let lcpService = LCPService( client: LCPClient(), licenseRepository: LCPSQLiteLicenseRepository(), passphraseRepository: LCPSQLitePassphraseRepository(), httpClient: DefaultHTTPClient() ) ``` -------------------------------- ### Iterate Through Search Results Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Search.md Use the `SearchIterator` to fetch search results page by page. Each page contains a `LocatorCollection` with an array of `Locator` objects. Call `iterator.next()` to get the next page; it returns `nil` when all results are exhausted or `.failure` if a read error occurs. ```swift while let page = try await iterator.next().get() { for locator in page.locators { print("Found result at: \(locator.href)") } } ``` -------------------------------- ### Assembling Navigator and SwiftUI Objects Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/SwiftUI.md Construct an EPUB navigator and assemble the SwiftUI view hierarchy by creating instances of ReaderViewControllerWrapper, ReaderViewController, and ReaderView. ```swift var config = EPUBNavigatorViewController.Configuration() config.editingActions.append( EditingAction( title: "Highlight", action: #selector(highlightSelection) ) ) let navigator = try EPUBNavigatorViewController( publication: publication, initialLocation: locator, config: config, ... ) // View model provided by your application. let viewModel = ReaderViewModel() let view = ReaderView( viewModel: viewModel, viewControllerWrapper: ReaderViewControllerWrapper( viewController: ReaderViewController( viewModel: viewModel, navigator: navigator ) ) ) ``` -------------------------------- ### Instantiate Visual Navigator Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Navigator.md Instantiate visual navigators like `EPUBNavigatorViewController` to display publication content within your app's view hierarchy. ```swift let navigator = try EPUBNavigatorViewController( publication: publication, initialLocation: lastReadLocation ) hostViewController.present(navigator, animated: true) ``` -------------------------------- ### Add New SPM Library Product and Targets Source: https://github.com/readium/swift-toolkit/blob/develop/MAINTAINING.md Define a new library product and its associated source and test targets in `Package.swift`. ```swift // products: .library(name: "Readium", targets: ["Readium"]), // targets: .target( name: "Readium", dependencies: ["ReadiumShared", "ReadiumNavigator"], path: "Sources/" ), .testTarget( name: "ReadiumTests", dependencies: ["Readium"], path: "Tests/Tests" ), ``` -------------------------------- ### Update LCPService Initialization for Keychain Repositories Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Replace SQLite-based LCP repositories with their Keychain equivalents when initializing LCPService. This enhances security and persistence. ```diff -import ReadiumAdapterLCPSQLite import ReadiumLCP let lcpService = LCPService( client: LCPClient(), - licenseRepository: try! LCPSQLiteLicenseRepository(), - passphraseRepository: try! LCPSQLitePassphraseRepository(), + licenseRepository: LCPKeychainLicenseRepository(), + passphraseRepository: LCPKeychainPassphraseRepository(), assetRetriever: assetRetriever, httpClient: httpClient ) ``` -------------------------------- ### Submit EPUB Preferences to Navigator Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Preferences.md Create a set of EPUB-specific preferences and submit them to the navigator. The navigator will then update its settings and presentation accordingly. You can read the new settings using the `settings` property. ```swift let preferences = EPUBPreferences( fontFamily: .serif, fontSize: 2.0, publisherStyles: false ) epubNavigator.submitPreferences(preferences) assert(epubNavigator.settings.fontFamily == .serif) ``` -------------------------------- ### Observe and Apply Highlight Decorations Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Highlights.md Observe changes in the highlight repository and apply them as decorations to the navigator. This ensures the UI stays in sync with the data. Decorations are mapped from highlight models, and the navigator handles diffing internally. Ensure UI updates run on the main thread. ```swift func observeHighlightDecorations() { guard let navigator = navigator as? DecorableNavigator else { return } // Register the tap callback once (see "Handling Taps" below) navigator.observeDecorationInteractions(inGroup: "highlights") { [weak self] event in self?.activateDecoration(event) } // Re-apply on every database change. highlightRepository.highlights(for: bookId) .receive(on: DispatchQueue.main) .sink { _ in } receiveValue: { [weak self] highlights in guard let self else { return } let decorations = highlights.map { highlight in Decoration( // Use your database primary key as the highlight's `id` — this is what // links the `Decoration` back to your model when the user later taps it. id: highlight.id, locator: highlight.locator, style: .highlight(tint: highlight.color) ) } navigator.apply(decorations: decorations, in: "highlights") } .store(in: &subscriptions) } ``` -------------------------------- ### Port Module Sources to Swift Toolkit Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Copy source files from legacy module forks into the appropriate directories within the new swift-toolkit repository. Obsolete files like Info.plist and .h files are removed. ```sh rm -rf Sources/*/* # Copy module sources cp -r ../r2-shared-swift/r2-shared-swift/* Sources/Shared cp -r ../r2-streamer-swift/r2-streamer-swift/* Sources/Streamer cp -r ../r2-navigator-swift/r2-navigator-swift/* Sources/Navigator cp -r ../r2-opds-swift/readium-opds/* Sources/OPDS cp -r ../r2-lcp-swift/readium-lcp-swift/* Sources/LCP # Remove obsolete files rm -rf Sources/*/Info.plist rm -rf Sources/*/*.h ``` -------------------------------- ### Implement a Basic Input Observer Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Input.md Implement InputObserving to log all pointer and key events. Return `true` to prevent subsequent observers from processing the event. ```swift navigator.addObserver(InputObserver()) @MainActor final class InputObserver: InputObserving { func didReceive(_ event: PointerEvent) async -> Bool { print("Received pointer event: \(event)") return false } func didReceive(_ event: KeyEvent) async -> Bool { print("Received key event: \(event)") return false } } ``` -------------------------------- ### Tag Release Script Source: https://github.com/readium/swift-toolkit/blob/develop/MAINTAINING.md Tag the new version in Git and push the tags. ```shell scripts/release-tag.sh ``` -------------------------------- ### Opening a Publication Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Open Publication.md Use PublicationOpener to create a Publication object from an Asset. Enable user interaction for LCP passphrase prompts. ```swift let result = await publicationOpener.open( asset: asset, allowUserInteraction: true, sender: sender ) ``` -------------------------------- ### Configure EPUB Navigator with Custom Fonts Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/EPUB Fonts.md Initialize `EPUBNavigatorViewController` with custom font declarations. Supports variable fonts and standard styles. ```swift let resources = FileURL(url: Bundle.main.resourceURL!)! let navigator = try EPUBNavigatorViewController( publication: publication, initialLocation: locator, config: .init( fontFamilyDeclarations: [ CSSFontFamilyDeclaration( fontFamily: .literata, fontFaces: [ // Literata is a variable font family, so we can provide a font weight range. // https://fonts.google.com/knowledge/glossary/variable_fonts CSSFontFace( file: resources.appendingPath("Literata-VariableFont_opsz,wght.ttf", isDirectory: false), style: .normal, weight: .variable(200...900) ), CSSFontFace( file: resources.appendingPath("Literata-Italic-VariableFont_opsz,wght.ttf", isDirectory: false), style: .italic, weight: .variable(200...900) ) ] ).eraseToAnyHTMLFontFamilyDeclaration(), CSSFontFamilyDeclaration( fontFamily: .atkinsonHyperlegible, fontFaces: [ CSSFontFace( file: resources.appendingPath("Atkinson-Hyperlegible-Regular.ttf", isDirectory: false), style: .normal, weight: .standard(.normal) ), CSSFontFace( file: resources.appendingPath("Atkinson-Hyperlegible-Italic.ttf", isDirectory: false), style: .italic, weight: .standard(.normal) ), CSSFontFace( file: resources.appendingPath("Atkinson-Hyperlegible-Bold.ttf", isDirectory: false), style: .normal, weight: .standard(.bold) ), CSSFontFace( file: resources.appendingPath("Atkinson-Hyperlegible-BoldItalic.ttf", isDirectory: false), style: .italic, weight: .standard(.bold) ), ] ).eraseToAnyHTMLFontFamilyDeclaration() ] ) ) ``` -------------------------------- ### Generate Xcode Project with xcodegen Source: https://github.com/readium/swift-toolkit/blob/develop/Tests/NavigatorTests/UITests/README.md Run this command in the specified directory to generate the Xcode project file from project.yml. ```bash cd Tests/NavigatorTests/UITests xcodegen generate ``` -------------------------------- ### EPUBReaderViewController for Highlights Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Highlights.md This Swift class integrates highlight management into an EPUB reader. It uses a `HighlightRepository` protocol for storage-agnostic highlight persistence and displays highlights as decorations within the navigator. The controller allows users to create new highlights from selections and interact with existing ones by tapping. ```swift import Combine import ReadiumNavigator import ReadiumShared import UIKit // MARK: - Data model struct Highlight { /// Database primary key (used as Decoration.id) var id: String var bookId: String var locator: Locator var color: UIColor } // MARK: - Storage protocol (implement with GRDB, CoreData, etc.) protocol HighlightRepository { func highlights(for bookId: String) -> AnyPublisher<[Highlight], Never> func highlight(forId id: String) async throws -> Highlight? func add(_ highlight: Highlight) async throws func remove(_ id: String) async throws } // MARK: - Reader view controller class EPUBReaderViewController: UIViewController { private let navigator: EPUBNavigatorViewController private let highlightRepository: HighlightRepository private let bookId: String private var subscriptions = Set() private let highlightDecorationGroup = "highlights" init( publication: Publication, bookId: String, lastLocation: Locator?, highlightRepository: HighlightRepository ) throws { self.bookId = bookId self.highlightRepository = highlightRepository navigator = try EPUBNavigatorViewController( publication: publication, initialLocation: lastLocation, config: EPUBNavigatorViewController.Configuration( editingActions: EditingAction.defaultActions + [ EditingAction(title: "Highlight", action: #selector(highlightSelection)) ] ) ) super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError() } override func viewDidLoad() { super.viewDidLoad() // Embed the navigator addChild(navigator) navigator.view.frame = view.bounds navigator.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(navigator.view) navigator.didMove(toParent: self) // Wire up highlights observeHighlightDecorations() } // MARK: - Displaying highlights private func observeHighlightDecorations() { guard let decorator = navigator as? DecorableNavigator else { return } decorator.observeDecorationInteractions(inGroup: highlightDecorationGroup) { [weak self] event in self?.activateDecoration(event) } highlightRepository.highlights(for: bookId) .receive(on: DispatchQueue.main) .sink { _ in } receiveValue: { [weak self] highlights in guard let self else { return } let decorations = highlights.map { h in Decoration( id: h.id, locator: h.locator, style: .highlight(tint: h.color) ) } decorator.apply(decorations: decorations, in: self.highlightDecorationGroup) } .store(in: &subscriptions) } // MARK: - Creating highlights @objc func highlightSelection() { guard let selection = navigator.currentSelection else { return } let highlight = Highlight( id: UUID().uuidString, bookId: bookId, locator: selection.locator, color: .yellow ) Task { try await highlightRepository.add(highlight) } navigator.clearSelection() } // MARK: - Tapping existing highlights private func activateDecoration(_ event: OnDecorationActivatedEvent) { let highlightId = event.decoration.id Task { guard let highlight = try? await highlightRepository.highlight(forId: highlightId) else { return } await MainActor.run { presentHighlightMenu(for: highlight, anchoredTo: event.rect) } } } private func presentHighlightMenu(for highlight: Highlight, anchoredTo rect: CGRect?) { let alert = UIAlertController(title: "Highlight", message: nil, preferredStyle: .actionSheet) alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in guard let self else { return } Task { try await self.highlightRepository.remove(highlight.id) } }) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) if let popover = alert.popoverPresentationController { popover.sourceView = view popover.sourceRect = rect ?? view.bounds popover.permittedArrowDirections = .down } present(alert, animated: true) } } ``` -------------------------------- ### Initialize Streamer with LCP Content Protection Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Initialize the `Streamer` with `LCPService.contentProtection()` to enable LCP support. This simplifies LCP handling by integrating it directly into the streamer. ```swift let lcpService = LCPService() let streamer = Streamer( contentProtections: [ lcpService.contentProtection() ] ) ``` -------------------------------- ### Observe Specific Keyboard Shortcuts Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Input.md Observe specific keyboard shortcuts by providing the key and optional modifiers to the .key observer factory. This allows for precise hotkey handling. ```swift navigator.addObserver(.key(.a) { self.log(.info, "User pressed A") return false }) ``` ```swift navigator.addObserver(.key(.a, [.control, .shift]) { self.log(.info, "User pressed Control+Shift+A") return false }) ``` -------------------------------- ### Create a Decoration Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Decorations.md Create a Decoration object by providing a unique ID, a locator, and a style. The ID should correspond to your model's primary key for easy lookup. ```swift let decoration = Decoration( id: highlight.id, locator: highlight.locator, style: .highlight(tint: highlight.color) ) ``` -------------------------------- ### Clone Legacy and New Forks Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Clone your existing Readium module forks and the new unified swift-toolkit repository into a dedicated migration directory. ```sh mkdir readium-migration cd readium-migration # Clone the legacy forks git clone https://github.com/USERNAME/r2-shared-swift.git git clone https://github.com/USERNAME/r2-streamer-swift.git git clone https://github.com/USERNAME/r2-navigator-swift.git git clone https://github.com/USERNAME/r2-opds-swift.git git clone https://github.com/USERNAME/r2-lcp-swift.git # Clone the new single fork git clone https://github.com/USERNAME/swift-toolkit.git ``` -------------------------------- ### Open Publication with ReadiumStreamer Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Getting Started.md Instantiate components and open a publication from a URL. Handles potential errors during asset retrieval and publication opening. The `allowUserInteraction` parameter is relevant for DRM. ```swift let httpClient = DefaultHTTPClient() let assetRetriever = AssetRetriever( httpClient: httpClient ) let publicationOpener = PublicationOpener( parser: DefaultPublicationParser( httpClient: httpClient, assetRetriever: assetRetriever, pdfFactory: DefaultPDFDocumentFactory() ) ) let url: URL = URL(...) // Retrieve an `Asset` to access the file content. switch await assetRetriever.retrieve(url: url.anyURL.absoluteURL!) { case .success(let asset): // Open a `Publication` from the `Asset`. switch await publicationOpener.open(asset: asset, allowUserInteraction: true, sender: view) { case .success(let publication): print("Opened \(publication.metadata.title)") case .failure(let error): // Failed to access or parse the publication } case .failure(let error): // Failed to retrieve the asset } ``` -------------------------------- ### Constructing an AssetRetriever Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Open Publication.md Instantiate an AssetRetriever with an HTTPClient to handle network requests for various URL schemes. ```swift let assetRetriever = AssetRetriever(httpClient: DefaultHTTPClient()) ``` -------------------------------- ### Observe Tap and Click Events Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Input.md Use ActivatePointerObserver's static factories to easily observe single taps or clicks. Modifiers can be specified to trigger the observer only when specific modifier keys are pressed. ```swift navigator.addObserver(.tap { event in print("User tapped at \(event.location)") return false }) ``` ```swift // Key modifiers can be used to recognize an event only when a modifier key is pressed. navigator.addObserver(.click(modifiers: [.shift]) { event in print("User clicked at \(event.location)") return false }) ``` -------------------------------- ### Perform a Search Query Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Search.md Initiate a search within a publication's content using the `search(query:options:)` method. This method returns a `SearchResult` which can be either a `SearchIterator` for paginating results or a `SearchError`. ```swift let result = await publication.search(query: "red panda") switch result { case .success(let iterator): // Use the iterator to page through results. case .failure(let error): handleSearchError(error) } ``` -------------------------------- ### Update EPUB Navigator Scripts Source: https://github.com/readium/swift-toolkit/blob/develop/MAINTAINING.md Update dependencies and regenerate scripts for the EPUB Navigator. ```shell cd Sources/Navigator/EPUB/Scripts pnpm update make scripts ``` -------------------------------- ### Configure Content Search Service with custom parameters Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Search.md Override the default `ContentSearchService` in the `onCreatePublication` callback to adjust parameters like `snippetLength` or to plug in a different `searchAlgorithm`. ```swift let result = await publicationOpener.open( asset: asset, allowUserInteraction: true, onCreatePublication: { _, _, services in services.setSearchServiceFactory( ContentSearchService.makeFactory( snippetLength: 300, searchAlgorithm: BasicStringSearchAlgorithm() ) ) } ) ``` -------------------------------- ### Check Navigator Support for Decoration Style Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Decorations.md Before enabling features that rely on specific decoration styles, verify if the navigator supports them using the `supports(decorationStyle:)` method. ```swift if navigator.supports(decorationStyle: .underline) { // Offer underlining in the UI } ``` -------------------------------- ### Update Build Tools Lockfile Source: https://github.com/readium/swift-toolkit/blob/develop/MAINTAINING.md Bump dependency versions in `BuildTools/Package.swift` and update the lockfile. ```shell swift package update --package-path BuildTools ``` -------------------------------- ### Acquire Publication from License Document Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Readium LCP.md Use the LCPService to acquire a protected publication from a License Document URL. Handles progress updates during download. ```swift let result = await lcpService.acquirePublication( from: .file(lcplURL), onProgress: { progress in switch progress { case .indefinite: // Display an activity indicator. case .percent(let percent): // Display a progress bar with percent from 0 to 1. } } ) switch result { case let .success(publication): // Import the `publication.localURL` file as any publication. case let .failure(error): // Display the error message. } ``` -------------------------------- ### Reset New Fork to 2.2.0 Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Reset the newly cloned swift-toolkit repository to the specific 2.2.0 release tag to prepare for porting changes. ```sh cd swift-toolkit git reset --hard 2.2.0 ``` -------------------------------- ### Clone Readium Swift Toolkit Repository Source: https://github.com/readium/swift-toolkit/blob/develop/TestApp/README.md Clone the Readium Swift toolkit repository to your local machine. This is the first step before generating Xcode project files. ```sh git clone https://github.com/readium/swift-toolkit.git cd swift-toolkit/TestApp ``` -------------------------------- ### Generate Xcode Project with Swift Package Manager Source: https://github.com/readium/swift-toolkit/blob/develop/TestApp/README.md Generate the Xcode project files using the Makefile with the 'spm' target. This command automatically downloads all necessary dependencies. ```sh make spm ``` -------------------------------- ### Integrate R2LCPClient Facade with LCPService Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Provides a facade to R2LCPClient.framework for use with ReadiumLCP's LCPService. This is necessary after the dependency was removed from r2-lcp-swift. ```swift import R2LCPClient import ReadiumLCP let lcpService = LCPService(client: LCPClient()) /// Facade to the private R2LCPClient.framework. class LCPClient: ReadiumLCP.LCPClient { func createContext(jsonLicense: String, hashedPassphrase: String, pemCrl: String) throws -> LCPClientContext { return try R2LCPClient.createContext(jsonLicense: jsonLicense, hashedPassphrase: hashedPassphrase, pemCrl: pemCrl) } func decrypt(data: Data, using context: LCPClientContext) -> Data? { return R2LCPClient.decrypt(data: data, using: context as! DRMContext) } func findOneValidPassphrase(jsonLicense: String, hashedPassphrases: [String]) -> String? { return R2LCPClient.findOneValidPassphrase(jsonLicense: jsonLicense, hashedPassphrases: hashedPassphrases) } } ``` -------------------------------- ### Check Asset Format Conformance Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Getting Started.md Use `asset.format.conformsTo()` to check if an asset is protected with LCP or represents an EPUB publication. This is useful for conditional logic based on asset type. ```swift if asset.format.conformsTo(.lcp) { // The asset is protected with LCP. } if asset.format.conformsTo(.epub) { // The asset represents an EPUB publication. } ``` -------------------------------- ### Apply Page List Decorations Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Decorations.md Build and apply page list decorations to the navigator. This function iterates through publication page list items, creates decorations with their locators and configurations, and applies them to the navigator. ```swift private func updatePageListDecorations() async { guard let navigator = navigator as? DecorableNavigator else { return } var decorations: [Decoration] = [] for (index, link) in publication.pageList.enumerated() { guard let title = link.title, let locator = await publication.locate(link) else { continue } decorations.append(Decoration( id: "page-list-\(index)", locator: locator, style: Decoration.Style( id: .pageList, config: PageListConfig(label: title) ) )) } navigator.apply(decorations: decorations, in: "page-list") } ``` -------------------------------- ### Stream LCP Protected Package Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Readium LCP.md Stream an LCP protected publication directly from a License Document (.lcpl) file by opening it with `PublicationOpener` and providing an `AssetRetriever` with an `HTTPClient`. ```swift let httpClient = DefaultHTTPClient() let assetRetriever = AssetRetriever(httpClient: httpClient) let publicationOpener = PublicationOpener( parser: DefaultPublicationParser( httpClient: httpClient, assetRetriever: assetRetriever ), contentProtections: [ lcpService.contentProtection(with: LCPDialogAuthentication()), ] ) // Retrieve an `Asset` to access the LCPL content. let url = FileURL(path: "/path/to/license.lcpl", isDirectory: false) let asset = try await assetRetriever.retrieve(url: url).get()as // Open a `Publication` from the LCPL `Asset`. let publication = try await publicationOpener.open( asset: asset, allowUserInteraction: true, sender: hostViewController ).get() print("Opened \(publication.metadata.title)") ``` -------------------------------- ### Check for supported search options Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Search.md Inspect `publication.searchOptions` to determine which search options the service actually supports. Do not display UI controls for options with a `nil` value, as they are not supported by the current service. ```swift let options = publication.searchOptions // Only show the case-sensitivity toggle if the service supports it. if let caseSensitive = options.caseSensitive { showCaseSensitiveToggle(defaultValue: caseSensitive) } ``` -------------------------------- ### Tagging a New Version in Git Source: https://github.com/readium/swift-toolkit/blob/develop/MAINTAINING.md Manually tag a new version in Git and push the tags to the remote repository. ```shell git checkout develop git pull git tag -a VERSION -m VERSION git push --tags ``` -------------------------------- ### Retrieving an Asset from a URL Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Open Publication.md Use AssetRetriever to fetch an Asset from a local file or an HTTP URL. Handle success or failure outcomes. ```swift // From a local file. let url = FileURL(string: "file:///path/to/book.epub") // or from an HTTP URL. let url = HTTPURL(string: "https://domain/book.epub") switch await assetRetriever.retrieve(url: url) { case .success(let asset): ... case .failure(let error): // Failed to retrieve the asset. } ``` -------------------------------- ### Update Carthage Dependencies for Swift Toolkit Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Replace existing r2-* swift library declarations with the new swift-toolkit repository in your Cartfile. This ensures Carthage uses the consolidated package. ```diff +github "readium/swift-toolkit" ~> 2.2.0 -github "readium/r2-shared-swift" ~> 2.2.0 -github "readium/r2-streamer-swift" ~> 2.2.0 -github "readium/r2-navigator-swift" ~> 2.2.0 -github "readium/r2-opds-swift" ~> 2.2.0 -github "readium/r2-lcp-swift" ~> 2.2.0 ``` -------------------------------- ### Navigate to a Specific Progression Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Navigator.md Obtain a Locator for a given publication progression (0.0 to 1.0) and use it to navigate the navigator. ```swift if let locator = await publication.locate(progression: 0.5) { await navigator.go(to: locator) } ``` -------------------------------- ### Apply Search Result Decorations Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Decorations.md Apply temporary 'search' group decorations for search results. Use index-based IDs and isActive to highlight the currently selected result. Clear the group when the search is dismissed. ```swift let navigator: DecorableNavigator func applySearchDecorations(selectedIndex: Int?) { let decorations = searchResults.enumerated().map { index, result in Decoration( id: "\(index)", locator: result.locator, style: .highlight(isActive: index == selectedIndex) ) } navigator.apply(decorations: decorations, in: "search") } // Show all results with none selected applySearchDecorations(selectedIndex: nil) // When the user moves to a result, mark it as active applySearchDecorations(selectedIndex: currentResultIndex) // Clear on dismiss navigator.apply(decorations: [], in: "search") ``` -------------------------------- ### Create and Store a Highlight from Text Selection Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Navigator/Highlights.md Implement the selector for the custom editing action to create a `Highlight` object from the current text selection. This involves retrieving the selection's locator and text, creating a `Highlight` model, and adding it to the repository. Ensure the selection is cleared after saving. ```swift @objc func highlightSelection() { guard let selection = navigator.currentSelection else { return } let highlight = Highlight( bookId: bookId, locator: selection.locator, text: selection.locator.text.highlight, color: .yellow ) Task { try await highlightRepository.add(highlight) // If you use a reactive pattern (see below), the navigator // updates automatically when the database changes. } // dismisses the text selection handles immediately after saving; // without it, the selection would linger on screen alongside the // newly rendered highlight decoration. navigator.clearSelection() } ``` -------------------------------- ### Commit Migrated Changes Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Migration Guide.md Stage and commit the ported source files to the new swift-toolkit repository with a descriptive commit message. ```sh git add Sources git commit -m "Apply local changes to Readium" ``` -------------------------------- ### Use a custom search algorithm Source: https://github.com/readium/swift-toolkit/blob/develop/docs/Guides/Search.md To use a custom algorithm that implements the `StringSearchAlgorithm` protocol, pass an instance of your custom algorithm to `ContentSearchService.makeFactory`. ```swift ContentSearchService.makeFactory(searchAlgorithm: MyFuzzySearchAlgorithm()) ```