-
Notifications
You must be signed in to change notification settings - Fork 192
Change publication detail to use SwiftUI #687
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request migrates the OPDS publication detail screen from UIKit to SwiftUI, modernizing the codebase and improving maintainability.
Changes:
- Replaced UIKit-based
OPDSPublicationInfoViewControllerwith a native SwiftUIOPDSPublicationInfoView - Removed the OPDS storyboard file and associated factory methods
- Implemented SwiftUI navigation with closure-based download handling and added a new error case for delegate availability
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| TestApp/Sources/OPDS/OPDSPublicationInfoViewController.swift | Removed UIKit view controller implementation |
| TestApp/Sources/OPDS/OPDSModule.swift | Updated to use SwiftUI navigation with closure-based download handler and added delegateNotAvailable error case |
| TestApp/Sources/OPDS/OPDSFeeds/OPDSPublicationInfoView.swift | Implemented new SwiftUI view with cover display, metadata, description, and download functionality |
| TestApp/Sources/OPDS/OPDSFactory.swift | Removed storyboard loading and view controller factory methods |
| TestApp/Sources/OPDS/Base.lproj/OPDS.storyboard | Removed storyboard file as it's no longer needed with SwiftUI |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private func download(link: ReadiumShared.Link) async { | ||
| isDownloading = true | ||
| do { | ||
| let book = try await download(link) { _ in } | ||
| alert = AlertMessage( | ||
| title: NSLocalizedString("success_title", comment: "Title of the alert when a publication is successfully downloaded"), | ||
| message: String(format: NSLocalizedString("library_download_success_message", comment: "Message of the alert when a publication is successfully downloaded"), book.title) | ||
| ) | ||
| } catch { | ||
| alert = AlertMessage( | ||
| title: NSLocalizedString("error_title", comment: "Title of the alert when an error occurred"), | ||
| message: error.localizedDescription | ||
| ) | ||
| } | ||
| isDownloading = false |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The isDownloading state is not reset in a defer block, which could leave it stuck in the downloading state if an unexpected error occurs. While the current code does set it to false after the do-catch block, using defer would be more robust and ensure it's always reset regardless of how the function exits.
| } | ||
|
|
||
| private extension Publication { | ||
| /// Finds the first link with `cover` or thumbnail relations. |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The coverLink property documentation states it finds the first link with "cover or thumbnail relations", but the implementation also checks for OPDS-specific image relations. The documentation should be updated to accurately reflect all the relations being checked.
| /// Finds the first link with `cover` or thumbnail relations. | |
| /// Finds the first link with a `cover` relation or an OPDS image relation | |
| /// (`http://opds-ps.org/image` or `http://opds-ps.org/image/thumbnail`). |
| Text("_________") | ||
| } | ||
|
|
||
| Text(publication.metadata.authors.map(\.name).joined(separator: ", ")) |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the main view, when the authors array is empty, the joined separator will create an empty string. Consider adding a check to only display the authors text when the array is not empty.
| Text(publication.metadata.authors.map(\.name).joined(separator: ", ")) | |
| if !publication.metadata.authors.isEmpty { | |
| Text(publication.metadata.authors.map(\.name).joined(separator: ", ")) | |
| } |
| enum OPDSError: Error { | ||
| case invalidURL(String) | ||
| case delegateNotAvailable |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new error case delegateNotAvailable should conform to LocalizedError protocol to provide a user-friendly error message. Currently, if this error is thrown and displayed to the user, it will show the raw case name instead of a helpful description.
| enum OPDSError: Error { | |
| case invalidURL(String) | |
| case delegateNotAvailable | |
| enum OPDSError: Error, LocalizedError { | |
| case invalidURL(String) | |
| case delegateNotAvailable | |
| var errorDescription: String? { | |
| switch self { | |
| case let .invalidURL(urlString): | |
| return NSLocalizedString( | |
| "The catalog URL is invalid: \(urlString).", | |
| comment: "Invalid OPDS catalog URL error" | |
| ) | |
| case .delegateNotAvailable: | |
| return NSLocalizedString( | |
| "Unable to download the publication because the OPDS delegate is not available.", | |
| comment: "OPDS delegate not available error" | |
| ) | |
| } | |
| } |
| } | ||
|
|
||
| struct AlertMessage: Identifiable { | ||
| var id: String { title + message } |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The computed id property concatenates title and message, which could lead to collisions if two different alerts happen to have the same concatenated string. This could prevent an alert from showing if the previous alert had the same id. Consider using UUID() instead for guaranteed uniqueness.
| var id: String { title + message } | |
| let id = UUID() |
| ProgressView() | ||
| .tint(.white) | ||
| } else { | ||
| Text("Download") |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The button text "Download" is hardcoded and not localized using NSLocalizedString, unlike the alert messages below. This creates an inconsistency in the localization approach and will prevent the button text from being translated for different locales.
| Text("Download") | |
| Text(NSLocalizedString("Download", comment: "Download button title")) |
|
|
||
| if let description = publication.metadata.description { | ||
| VStack(alignment: .leading, spacing: 10) { | ||
| Text("About") |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The section heading "About" is hardcoded and not localized using NSLocalizedString, unlike other user-facing strings in this file. This will prevent the heading from being translated for different locales.
| Text("About") | |
| Text(NSLocalizedString("About", comment: "Section heading for the publication description")) |
| } | ||
|
|
||
| func updateUIViewController(_ uiViewController: OPDSPublicationInfoViewController, context: Context) {} | ||
| private func download(link: ReadiumShared.Link) async { |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name "download" is used for both the closure property (line 12) and the method (line 123). While this is technically valid Swift due to different signatures, it can be confusing and makes the code harder to understand. Consider renaming the method to something like "performDownload" or "downloadPublication" to clearly distinguish it from the closure property.
| private func download(link: ReadiumShared.Link) async { | |
| private func performDownload(link: ReadiumShared.Link) async { |
| .fixedSize(horizontal: false, vertical: true) | ||
| .multilineTextAlignment(.leading) | ||
|
|
||
| Text(publication.metadata.authors.map(\.name).joined(separator: ", ")) |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the authors array is empty, the joined separator will create an empty string, which will still display an empty Text view. Consider adding a check to only display the authors Text when the array is not empty, or provide a fallback text like "Unknown Author" for better UX.
| Text(publication.metadata.authors.map(\.name).joined(separator: ", ")) | |
| let authorsText = publication.metadata.authors.map(\.name).joined(separator: ", ") | |
| Text(authorsText.isEmpty ? "Unknown Author" : authorsText) |
|
I'm closing this PR as rewriting the Test App in SwiftUI is not on the table. We intend to remove the Test App and replace it with a more technical application for testing the Readium toolkit. |
No description provided.