A Swift package for reading and writing ComicInfo.xml files, following ComicInfo schema specifications from the Anansi Project.
- 📚 Complete Schema Support: Full ComicInfo v2.0 schema implementation
- 🔧 Idiomatic Swift API: Swift-style interface with proper naming conventions
- 📁 Flexible Loading: Load from file paths, URLs, or XML strings
- 🌍 Unicode Support: Full Unicode and special character handling
- 📖 Manga Support: Right-to-left reading direction and manga-specific fields
- ✅ Comprehensive Validation: Schema-compliant enum validation and type coercion
- 🚨 Detailed Error Handling: Swift error types with helpful error messages
- 📊 Export Support: JSON and property list serialization support
- ⚡ Swift 6 Ready: Built with Swift 6.2+ and modern concurrency support
- Swift 6.2+
- Foundation
- XMLParser (included in Foundation)
Add the following to your Package.swift file:
dependencies: [
.package(url: "https://github.com/your-org/ComicInfo-swift.git", from: "1.0.0")
]Or add it through Xcode:
- File → Add Package Dependencies
- Enter the repository URL
- Choose your version requirements
import ComicInfo
// Load from file path
let comic = try ComicInfo.load(from: "/path/to/ComicInfo.xml")
// Load from URL
let url = URL(fileURLWithPath: "/path/to/ComicInfo.xml")
let comic = try ComicInfo.load(from: url)
// Load asynchronously (Swift 6.2+)
let comic = try await ComicInfo.load(from: url)
// Load from XML string
let xmlString = """
<ComicInfo>
<Title>Amazing Spider-Man</Title>
<Series>Amazing Spider-Man</Series>
<Number>1</Number>
<Year>2023</Year>
</ComicInfo>
"""
let comic = try ComicInfo.load(fromXML: xmlString)let issue = try ComicInfo.load(from: "ComicInfo.xml")
// Basic properties
print("Title: \(comic.title ?? "Unknown")")
print("Series: \(comic.series ?? "Unknown")")
print("Issue #: \(comic.number ?? "Unknown")")
print("Year: \(comic.year ?? 0)")
// Creator information
print("Writer: \(comic.writer ?? "Unknown")")
print("Artist: \(comic.penciller ?? "Unknown")")
print("Publisher: \(comic.publisher ?? "Unknown")")
// Multi-value fields (comma-separated in XML)
let genres = comic.genres // ["Action", "Adventure", "Superhero"]
let characters = comic.characters // ["Spider-Man", "Peter Parker"]
let locations = comic.locations // ["New York", "Manhattan"]
// Boolean helpers
if comics.isManga {
print("This is a manga")
if comics.isRightToLeft {
print("Read right-to-left")
}
}
if comics.isBlackAndWhite {
print("Black and white comic")
}
// Publication date
if let pubDate = comic.publicationDate {
print("Published: \(pubDate)")
}let issue = try ComicInfo.load(from: "ComicInfo.xml")
// Check if issue has page information
if comic.hasPages {
print("Total pages: \(comic.pages.count)")
// Filter pages by type
let coverPages = comic.coverPages
let storyPages = comic.storyPages
print("Cover pages: \(coverPages.count)")
print("Story pages: \(storyPages.count)")
// Access individual pages
for page in comic.pages {
print("Page \(page.image): \(page.type)")
if page.isCover {
print(" This is a cover page")
}
if page.isDoublePage {
print(" Double-page spread")
}
if let (width, height) = page.dimensions,
page.dimensionsAvailable {
print(" Size: \(width)x\(height)")
if let ratio = page.aspectRatio {
print(" Aspect ratio: \(ratio)")
}
}
}
}let comic = try ComicInfo.load(from: "ComicInfo.xml")
// Export to JSON string
let jsonString = try comic.toJSONString()
print(jsonString)
// Export to JSON data
let jsonData = try comic.toJSONData()
try jsonData.write(to: URL(fileURLWithPath: "output.json"))
// Round-trip: JSON -> Issue
let decoder = JSONDecoder()
let reimported = try decoder.decode(ComicInfo.comic.self, from: jsonData)let comic = ComicInfo.Issue(
title: "My Comic",
series: "My Series",
number: "1",
year: 2023,
writer: "John Doe"
)
// Export to XML string
let xmlString = try comic.toXMLString()
print(xmlString)
// Save to file
try xmlString.write(
to: URL(fileURLWithPath: "ComicInfo.xml"),
atomically: true,
encoding: .utf8
)
// Round-trip: XML -> Issue -> XML
let reimported = try ComicInfo.load(fromXML: xmlString)
let xmlString2 = try reimported.toXMLString()do {
let comic = try ComicInfo.load(from: "ComicInfo.xml")
print("Loaded: \(comic.title ?? "Unknown")")
} catch ComicInfoError.fileError(let message) {
print("File error: \(message)")
} catch ComicInfoError.parseError(let message) {
print("Parse error: \(message)")
} catch ComicInfoError.invalidEnum(let field, let value, let validValues) {
print("Invalid \(field): '\(value)'. Valid values: \(validValues)")
} catch ComicInfoError.rangeError(let field, let value, let min, let max) {
print("\(field) value '\(value)' out of range (\(min)..\(max))")
} catch {
print("Other error: \(error)")
}import ComicInfo
// Create a new comic issue
let comic = ComicInfo.Issue(
ageRating: .teen,
colorist: "Steve Oliff",
charactersRawData: "Spider-Man, Peter Parker, Mary Jane Watson",
communityRating: 4.5,
count: 100,
coverArtist: "Todd McFarlane",
day: 15,
genreRawData: "Superhero, Action, Adventure",
inker: "Todd McFarlane",
languageISO: "en",
letterer: "Rick Parker",
locationsRawData: "New York City, Manhattan",
manga: .no,
month: 8,
notes: "First appearance of Venom",
number: "300",
pageCount: 22,
penciller: "Todd McFarlane",
publisher: "Marvel Comics",
series: "The Amazing Spider-Man",
summary: "Spider-Man faces his greatest challenge yet...",
title: "The Amazing Spider-Man",
volume: 1,
writer: "David Michelinie",
year: 1988,
pages: [
ComicInfo.Page(image: 0, type: .frontCover),
ComicInfo.Page(image: 1, type: .story),
ComicInfo.Page(image: 2, type: .story),
// ... more pages
ComicInfo.Page(image: 21, type: .backCover)
]
)
// Export to XML
let xml = try comic.toXMLString()
try xml.write(to: URL(fileURLWithPath: "ComicInfo.xml"),
atomically: true, encoding: .utf8)The main namespace containing all types and loading methods.
load(from: String)- Smart load from file path or XML stringload(from: URL)- Load from file URLload(from: URL) async- Async load from URLload(fromXML: String)- Load from XML string
Represents a comic book issue with all metadata.
Basic Info:
title: String?- Issue titleseries: String?- Series namenumber: String?- Issue numbervolume: Int?- Volume numbercount: Int?- Total issues in seriesyear: Int?- Publication yearmonth: Int?- Publication month (1-12)day: Int?- Publication day (1-31)
Creator Fields:
writer: String?- Writer(s)penciller: String?- Penciller(s)inker: String?- Inker(s)colorist: String?- Colorist(s)letterer: String?- Letterer(s)coverArtist: String?- Cover artist(s)editor: String?- Editor(s)translator: String?- Translator(s)
Publication Info:
publisher: String?- Publisher nameimprint: String?- Imprint nameformat: String?- Publication formatlanguageISO: String?- Language code
Content Fields:
summary: String?- Story summarynotes: String?- Additional notesreview: String?- Review textcommunityRating: Double?- Rating (0.0-5.0)ageRating: AgeRating?- Age rating enumblackAndWhite: BlackAndWhite?- B&W statusmanga: Manga?- Manga/reading direction
Multi-value Fields (String):
charactersRawData: String?- Characters (comma-separated)teamsRawData: String?- Teams (comma-separated)locationsRawData: String?- Locations (comma-separated)genreRawData: String?- Genres (comma-separated)webRawData: String?- Web URLs (space-separated)
Multi-value Fields (Arrays):
characters: [String]- Parsed character namesteams: [String]- Parsed team nameslocations: [String]- Parsed location namesgenres: [String]- Parsed genreswebUrls: [URL]- Parsed web URLs
Story Arc Fields:
storyArc: String?- Story arc namestoryArcNumber: String?- Position in arcstoryArcs: [String]- Multiple story arcsstoryArcNumbers: [String]- Arc positions
Page Info:
pages: [Page]- Page arraypageCount: Int?- Total page count
isManga: Bool- True if manga formatisRightToLeft: Bool- True if right-to-left readingisBlackAndWhite: Bool- True if black and whitehasPages: Bool- True if pages array is not emptycoverPages: [Page]- Filter to cover pages onlystoryPages: [Page]- Filter to story pages onlypublicationDate: Date?- Computed publication date
toJSONString() throws -> String- Export to JSON stringtoJSONData() throws -> Data- Export to JSON datatoXMLString() throws -> String- Export to XML string
Represents a single page in a comic.
image: Int- Page number/indextype: PageType- Page type enumdoublePage: Bool- Double-page spread flagimageSize: Int- File size in byteskey: String- Key/identifierbookmark: String- Bookmark textimageWidth: Int- Image width (-1 if unknown)imageHeight: Int- Image height (-1 if unknown)
isCover: Bool- True if cover page typeisStory: Bool- True if story page typeisDeleted: Bool- True if deleted page typeisDoublePage: Bool- Same asdoublePageisBookmarked: Bool- True if bookmark is setdimensions: (width: Int?, height: Int?)- Optional dimensionsdimensionsAvailable: Bool- True if both dimensions knownaspectRatio: Double?- Width/height ratio if available
.unknown.adultsOnly18Plus.earlyChildhood.everyone.everyone10Plus.g.kidsToAdults.m.ma15Plus.mature17Plus.pg.r18Plus.ratingPending.teen.x18Plus
.unknown.no.yes.yesAndRightToLeft
.unknown.no.yes
.frontCover.innerCover.roundup.story.advertisement.editorial.letters.preview.backCover.other.deleted
All errors conform to ComicInfoError enum:
.fileError(String)- File access errors.parseError(String)- XML parsing errors.invalidEnum(field:value:validValues:)- Invalid enum values.rangeError(field:value:min:max:)- Numeric range violations.typeCoercionError(field:value:expectedType:)- Type conversion errors.schemaError(String)- Schema validation errors
- macOS 26+ (Tahoe)
- iOS 26.0+
This project is licensed under the MIT License - see the LICENSE file for details.
- Anansi Project for ComicInfo schema specification
- ComicRack for the original ComicInfo.xml format
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Follow TDD practices - write tests first
- Ensure all tests pass (
swift test) - Run
swift-formaton your code - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
swift test# Generate Xcode project first
swift package generate-xcodeproj
# Run on iOS Simulator
xcodebuild test \
-project ComicInfo.xcodeproj \
-scheme ComicInfo-Package \
-destination "platform=iOS Simulator,name=iPhone 26,OS=26.0"This project uses swift-format for code formatting:
# Check formatting
swift-format lint --recursive Sources Tests
# Auto-format code
swift-format format --recursive Sources Tests --in-placeValidate the package structure and dependencies:
# Describe package structure
swift package describe --type json
# Resolve dependencies
swift package resolve
# Show dependency tree
swift package show-dependencies
# Build in debug mode
swift build --configuration debug
# Build in release mode
swift build --configuration releaseThe project uses GitHub Actions for CI with the following checks:
- macOS Tests: Run full test suite on macOS 26
- iOS Tests: Run tests on iOS 26 simulators (iPhone and iPad)
- Code Formatting: Verify code follows formatting standards
- Package Validation: Ensure package can be resolved and built
CI runs on every push to main branches and on pull requests.