Swift SDK for the Metabind API. Fetch and render dynamic content in your iOS/iPadOS/macOS apps.
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/metabind/metabind-apple.git", from: "1.0.0")
]Create a MetabindClient and inject it into the SwiftUI environment:
import SwiftUI
import Metabind
@main
struct MyApp: App {
@State var client = MetabindClient(
url: URL(string: "https://api.metabind.ai/graphql")!,
ws: URL(string: "wss://api.metabind.ai/graphql")!,
apiKey: "your-api-key",
organizationId: "your-org-id",
projectId: "your-project-id"
)
var body: some Scene {
WindowGroup {
ContentView()
.environment(client)
}
}
}MetabindView reads MetabindClient from the environment automatically. It handles fetching, caching, and rendering:
import SwiftUI
import Metabind
struct ContentScreen: View {
var body: some View {
MetabindView(contentId: "cont_123")
}
}Features:
- Automatic loading, error, and success states
- SQLite-backed caching with cache-then-network updates
- Optional real-time subscriptions via WebSocket
- Task lifecycle tied to view (automatic cancellation)
With Real-time Updates:
// Enable WebSocket subscription for live updates
MetabindView(contentId: "cont_123", enableSubscription: true)When enableSubscription: true, the view runs both the content stream (for initial load) and a WebSocket subscription (for real-time updates from the CMS).
For more control, use the MetabindClient APIs directly.
For complete API documentation, see the Metabind GraphQL Documentation. You can also view the GraphQL schema in this repository.
All APIs follow a consistent pattern:
fetch*()- Single async/await requeststream*()- AsyncStream that yields cached data first, then network updatessubscribeTo*()- Real-time WebSocket subscription (content only)
Fetch and display individual content entries.
// Fetch single content
let content = try await client.fetchContent(id: "cont_123")
// Stream content (cache -> network)
for await result in client.streamContent(id: "cont_123") {
switch result {
case .success(let content):
// Update UI
case .failure(let error):
// Handle error
}
}
// Subscribe to real-time updates
for await result in client.subscribeToContent(id: "cont_123") {
switch result {
case .success(let content):
// Handle live update
case .failure(let error):
// Handle error
}
}Fetch paginated lists of content with filtering and sorting.
// Fetch contents with filtering
let contentsList = try await client.fetchContents(
typeId: "type_123", // Filter by content type
tags: ["featured", "new"], // Filter by tags
locale: "en-US", // Filter by locale
search: "hello", // Text search
filter: ContentFilter(...), // Advanced filtering
sort: [SortCriteria(...)], // Sorting
cursor: nil, // Pagination cursor
limit: 20 // Page size
)
let items = contentsList.data
let hasMore = contentsList.pagination.hasMore
let nextCursor = contentsList.pagination.cursor
// Stream contents list
for await result in client.streamContents(typeId: "type_123", tags: ["featured"]) {
// Handle updates
}Fetch component definitions.
// Fetch single component
let component = try await client.fetchComponent(id: "comp_123")
// Fetch component list with search
let components = try await client.fetchComponents(
search: "Button",
cursor: nil,
limit: 20
)
// Stream components
for await result in client.streamComponents(search: "Card") {
// Handle updates
}Fetch content type schemas.
// Fetch single content type
let contentType = try await client.fetchContentType(id: "type_123")
let schema = try contentType.parsedSchema() // Parse schema JSON
// Fetch content type list
let contentTypes = try await client.fetchContentTypes(
search: "Article",
cursor: nil,
limit: 20
)
// Stream content types
for await result in client.streamContentTypes() {
// Handle updates
}Fetch media assets (images, videos, files).
// Fetch single asset
let asset = try await client.fetchAsset(id: "asset_123")
// Fetch asset list with filtering
let assets = try await client.fetchAssets(
type: "image/jpeg", // Filter by MIME type
tags: ["hero", "banner"], // Filter by tags
search: "logo", // Text search
filter: AssetFilter(...), // Advanced filtering
sort: [SortCriteria(...)], // Sorting
cursor: nil,
limit: 20
)
// Stream assets
for await result in client.streamAssets(type: "image/png") {
// Handle updates
}Fetch tags used for organizing content and assets.
// Fetch single tag
let tag = try await client.fetchTag(id: "tag_123")
// Fetch tag list
let tags = try await client.fetchTags(
search: "category",
cursor: nil,
limit: 20
)
// Stream tags
for await result in client.streamTags() {
// Handle updates
}Fetch published component packages (versioned snapshots).
// Fetch single package by version
let package = try await client.fetchPackage(version: "1.0.0")
// Fetch package list
let packages = try await client.fetchPackages(
cursor: nil,
limit: 20
)
// Stream packages
for await result in client.streamPackages() {
// Handle updates
}Execute pre-configured searches created in the CMS.
// Fetch saved search definition
let savedSearch = try await client.fetchSavedSearch(id: "search_123")
// Fetch saved search list
let savedSearches = try await client.fetchSavedSearches(
type: .case(.CONTENT), // or .ASSET
cursor: nil,
limit: 20
)
// Execute saved search and get results
let results = try await client.executeSavedSearch(
id: "search_123",
cursor: nil,
limit: 20
)
// Results are a union type (ContentList or AssetList)
if let contentList = results.asContentList {
let contents = contentList.data
} else if let assetList = results.asAssetList {
let assets = assetList.data
}For advanced use cases, fetch content and render with BindJSView directly:
import SwiftUI
import Metabind
import BindJS
struct CustomContentView: View {
@Environment(MetabindClient.self) var client
@State private var resolvedContent: ResolvedContent?
let contentId: String
var body: some View {
Group {
if let content = resolvedContent {
BindJSView(content: content)
} else {
ProgressView()
}
}
.task {
await loadContent()
}
}
func loadContent() async {
do {
let content = try await client.fetchContent(id: contentId)
resolvedContent = try await content.resolvedContent(using: client)
} catch {
print("Error: \(error)")
}
}
}Extract structured data from content for application logic:
let content = try await client.fetchContent(id: "cont_123")
// Parse content JSON into PropertyValue map
let propertyMap = try content.parsedContent()
// Access nested data
let title = propertyMap["title"]?.string
let items = propertyMap["items"]?.array
let metadata = propertyMap["metadata"]?.objectControl caching behavior with CachePolicy:
| Policy | Description | Use Case |
|---|---|---|
.fetchIgnoringCacheData |
Always fetch from network | Default for fetch*() methods |
.returnCacheDataDontFetch |
Only return cached data | Offline mode, instant display |
.returnCacheDataAndFetch |
Yield cache, then network | Only for stream*() methods |
.returnCacheDataElseFetch |
Cache if available, else network | Avoid with fetch*() |
// Force network fetch
let content = try await client.fetchContent(
id: "cont_123",
cachePolicy: .fetchIgnoringCacheData
)
// Try cache only (for instant display)
let cached = try await client.fetchContent(
id: "cont_123",
cachePolicy: .returnCacheDataDontFetch
)do {
let content = try await client.fetchContent(id: "cont_123")
} catch MetabindClientError.noData {
// Content not found
} catch MetabindClientError.graphQLErrors(let messages) {
// GraphQL errors (e.g., validation, permissions)
print("Errors: \(messages)")
} catch MetabindClientError.missingResolvedPackage {
// Package resolution failed
} catch MetabindClientError.invalidComponentsJSON {
// Package component parsing failed
} catch {
// Network or other errors
}- SQLite Cache: Persistent cache in app documents directory
- Authentication: Automatic API key injection for HTTP and WebSocket
- WebSocket Support: Real-time subscriptions with auto-reconnect
- Async/Await: Modern Swift concurrency throughout
- Type Safety: Fully generated types from GraphQL schema
- SwiftUI Integration: Purpose-built views for Metabind content
- Apollo iOS 1.23.0 (GraphQL client with WebSocket and SQLite support)
- BindJS (component rendering engine)
When the GraphQL schema or queries change:
./apollo-ios-cli generateGenerated code is placed in Sources/Metabind/generated/. Never edit these files manually.