SwiftFFetch is a Swift library for fetching and processing content from AEM (.live) Content APIs and similar JSON-based endpoints. It is designed for composable applications, making it easy to retrieve, paginate, and process content in a Swift-native way.
- Swift-native API: Designed for idiomatic use in Swift projects.
- Async/Await Support: Uses Swift concurrency for efficient, modern code.
- Pagination: Handles paginated endpoints seamlessly.
- Composable: Chainable methods for mapping, filtering, and transforming content.
- HTTP Caching: Intelligent caching with respect for HTTP cache control headers.
- Sheet Selection: Access specific sheets in multi-sheet JSON resources.
- Extensible: Easily integrate with your own models and workflows.
Add SwiftFFetch to your Package.swift dependencies:
.package(url: "https://github.com/your-org/swffetch.git", from: "1.0.0")Then add "SwiftFFetch" to your target dependencies.
import SwiftFFetch
let entries = FFetch(url: "https://example.com/query-index.json")
for try await entry in entries {
print(entry["title"] as? String ?? "")
}let firstEntry = try await FFetch(url: "https://example.com/query-index.json").first()
print(firstEntry?["title"] as? String ?? "")let allEntries = try await FFetch(url: "https://example.com/query-index.json").all()
print("Total entries: \(allEntries.count)")SwiftFFetch includes comprehensive HTTP caching support that respects server cache control headers by default and allows for custom cache configurations.
By default, SwiftFFetch uses a shared memory cache and respects HTTP cache control headers:
// First request fetches from server
let entries1 = try await FFetch(url: "https://example.com/api/data.json").all()
// Second request uses cache if server sent appropriate cache headers
let entries2 = try await FFetch(url: "https://example.com/api/data.json").all()Use the .cache() method to configure caching behavior:
// Always fetch fresh data (bypass cache)
let freshData = try await FFetch(url: "https://example.com/api/data.json")
.cache(.noCache)
.all()
// Only use cached data (won't make network request)
let cachedData = try await FFetch(url: "https://example.com/api/data.json")
.cache(.cacheOnly)
.all()
// Use cache if available, otherwise load from network
let data = try await FFetch(url: "https://example.com/api/data.json")
.cache(.cacheElseLoad)
.all()Create your own cache with specific memory and disk limits:
let customCache = URLCache(
memoryCapacity: 10 * 1024 * 1024, // 10MB
diskCapacity: 50 * 1024 * 1024 // 50MB
)
let customConfig = FFetchCacheConfig(
policy: .useProtocolCachePolicy,
cache: customCache,
maxAge: 3600 // Cache for 1 hour regardless of server headers
)
let data = try await FFetch(url: "https://example.com/api/data.json")
.cache(customConfig)
.all()The cache is reusable between multiple FFetch calls and can be shared with other HTTP requests:
// Create a shared cache for your application
let appCache = URLCache(memoryCapacity: 20 * 1024 * 1024, diskCapacity: 100 * 1024 * 1024)
let config = FFetchCacheConfig(cache: appCache)
// Use with FFetch
let ffetchData = try await FFetch(url: "https://api.example.com/data.json")
.cache(config)
.all()
// Use the same cache with URLSession
let sessionConfig = URLSessionConfiguration.default
sessionConfig.urlCache = appCache
let session = URLSession(configuration: sessionConfig)Legacy cache methods are still supported:
// Legacy method - maps to .cache(.noCache)
let freshData = try await FFetch(url: "https://example.com/api/data.json")
.reloadCache()
.all()
// Legacy method with parameter
let data = try await FFetch(url: "https://example.com/api/data.json")
.withCacheReload(false) // Uses default cache behavior
.all()let allEntries = try await FFetch(url: "https://example.com/query-index.json").all()
allEntries.forEach { entry in
print(entry)
}let filteredTitles = FFetch(url: "https://example.com/query-index.json")
.map { $0["title"] as? String }
.filter { $0?.contains("Swift") == true }
for try await title in filteredTitles {
print(title ?? "")
}let limitedEntries = FFetch(url: "https://example.com/query-index.json")
.chunks(100)
.limit(5)
for try await entry in limitedEntries {
print(entry)
}let productEntries = FFetch(url: "https://example.com/query-index.json")
.sheet("products")
for try await entry in productEntries {
print(entry["sku"] as? String ?? "")
}SwiftFFetch provides a follow() method to fetch HTML documents referenced in your data. For security, document following is restricted to the same hostname as your initial request by default.
// Basic document following (same hostname only)
let entriesWithDocs = try await FFetch(url: "https://example.com/query-index.json")
.follow("path", as: "document") // follows URLs in 'path' field
.all()
// The 'document' field will contain parsed HTML Document objects
for entry in entriesWithDocs {
if let doc = entry["document"] as? Document {
print(doc.title())
}
}To allow document following to other hostnames, use the allow() method:
// Allow specific hostname
let entries = try await FFetch(url: "https://example.com/query-index.json")
.allow("trusted.com")
.follow("path", as: "document")
.all()
// Allow multiple hostnames
let entries = try await FFetch(url: "https://example.com/query-index.json")
.allow(["trusted.com", "api.example.com"])
.follow("path", as: "document")
.all()
// Allow all hostnames (use with caution)
let entries = try await FFetch(url: "https://example.com/query-index.json")
.allow("*")
.follow("path", as: "document")
.all()The hostname restriction is an important security feature that prevents:
- Cross-site request forgery (CSRF): Malicious sites cannot trick your app into fetching arbitrary content
- Data exfiltration: Prevents accidental requests to untrusted domains
- Server-side request forgery (SSRF): Reduces risk of unintended internal network access
By default, only the hostname of your initial JSON request is allowed. This mirrors the security model used by web browsers for cross-origin requests.
The query-index.json files used in the examples above are typically generated by AEM Live sites as part of their content indexing process. For more information about how these index files are created and structured, see the AEM Live Indexing Documentation.
See Examples.swift in the repository for more detailed usage.
This project is licensed under the terms of the Apache License 2.0. See LICENSE for details.
Use the provided Makefile for common development tasks:
# Run all tests
make test
# Run tests with coverage
make coverage
# Generate detailed coverage report
make coverage-report
# Run swiftlint
make lint
# Build the project
make build
# Clean build artifacts
make clean
# Install dependencies
make install
# Format code (requires swiftformat)
make formatThis project uses SwiftLint as a pre-commit hook to ensure code quality. The hook automatically runs before each commit and will prevent commits if there are any linting violations.
To bypass the pre-commit hook (not recommended), use:
git commit --no-verifyTo set up the pre-commit hook automatically, run:
./scripts/setup-pre-commit-hook.shThis script will:
- Check if SwiftLint is installed (and install it via Homebrew if needed)
- Install the pre-commit hook
- Test the hook to ensure it works correctly