Swift SDK -- Content Operations
Swift SDK -- Content Operations
CRUD operations, content delivery, content trees, media upload, the query API, and SwiftUI integration patterns.
Generic Resource CRUD
Most entities on ModulaClient are exposed as Resource<Entity, CreateParams, UpdateParams, ID>. Every Resource provides the same seven methods:
| Method | Signature | Description |
|---|---|---|
list() |
async throws -> [Entity] |
List all entities |
get(id:) |
async throws -> Entity |
Get one by ID |
create(params:) |
async throws -> Entity |
Create and return the new entity |
update(params:) |
async throws -> Entity |
Update and return the modified entity |
delete(id:) |
async throws |
Delete by ID |
listPaginated(params:) |
async throws -> PaginatedResponse<Entity> |
List with pagination |
count() |
async throws -> Int64 |
Count all entities |
There is also rawList(queryItems:) which returns raw Data for custom query parameters.
List
let datatypes = try await client.datatypes.list()
Get by ID
let datatype = try await client.datatypes.get(id: DatatypeID("01HXYZ..."))
Create
let newDatatype = try await client.datatypes.create(params: CreateDatatypeParams(
name: "blog_post",
label: "Blog Post"
))
Update
let updated = try await client.datatypes.update(params: UpdateDatatypeParams(
datatypeID: DatatypeID("01HXYZ..."),
name: "blog_post",
label: "Blog Post (Updated)"
))
Delete
try await client.datatypes.delete(id: DatatypeID("01HXYZ..."))
Paginated List
let page = try await client.datatypes.listPaginated(params: PaginationParams(
limit: 20,
offset: 0
))
print("Page has \(page.data.count) items out of \(page.total) total")
PaginatedResponse<T> contains:
| Field | Type | Description |
|---|---|---|
data |
[T] |
Items in the current page |
total |
Int64 |
Total count across all pages |
limit |
Int64 |
Requested page size |
offset |
Int64 |
Requested offset |
Count
let total = try await client.datatypes.count()
NoCreate Resources
Media uses NoCreate as its create params type because you create media via multipart upload, not JSON POST. Calling create(params:) on client.media produces a compile error.
// Upload media instead:
let media = try await client.mediaUpload.upload(
data: imageData,
filename: "photo.jpg"
)
// List and get work normally:
let allMedia = try await client.media.list()
let one = try await client.media.get(id: MediaID("01HXYZ..."))
Content Delivery
ContentDeliveryResource retrieves published content by slug. This is the primary read path for frontend applications.
let pageData = try await client.content.getPage(slug: "blog/hello-world")
getPage returns raw Data because the response shape varies based on the format parameter.
| Parameter | Type | Default | Description |
|---|---|---|---|
slug |
String |
required | URL path to the content. Leading / is stripped. |
format |
String |
"" |
Response format: contentful, sanity, strapi, wordpress, clean, raw. Empty uses server default. |
locale |
String |
"" |
Locale code for localized content. Empty uses default locale. |
let data = try await client.content.getPage(
slug: "blog/hello-world",
format: "clean",
locale: "en"
)
// Decode the response based on your chosen format
let page = try JSON.decoder.decode(YourPageModel.self, from: data)
Query API
QueryResource provides structured content queries by datatype name with sorting, filtering, pagination, and locale support.
let result = try await client.query.query(
datatype: "blog_post",
params: QueryParams(
sort: "-date_created",
limit: 10,
offset: 0,
locale: "en",
status: "published",
filters: ["category": "tech"]
)
)
QueryParams fields:
| Field | Type | Default | Description |
|---|---|---|---|
sort |
String |
"" |
Sort field. Prefix with - for descending. |
limit |
Int |
0 |
Max items to return. 0 uses server default. |
offset |
Int |
0 |
Number of items to skip. |
locale |
String |
"" |
Locale code filter. |
status |
String |
"" |
Content status filter (draft, published). |
filters |
[String: String] |
[:] |
Field-level filters passed as query parameters. |
QueryResult contains:
| Field | Type | Description |
|---|---|---|
data |
[QueryItem] |
Matching content items |
total |
Int |
Total matching count |
limit |
Int |
Applied limit |
offset |
Int |
Applied offset |
datatype |
QueryDatatype |
Datatype metadata (name, label) |
Each QueryItem provides contentDataID, datatypeID, authorID, status, dates, and a fields dictionary mapping field names to string values.
Content Trees
Content in ModulaCMS is organized as hierarchical trees that you can read, save, and restructure.
Reading Trees
Use adminTree to fetch a full tree by slug:
let treeData = try await client.adminTree.get(slug: "blog", format: "clean")
Returns raw Data that you decode based on your format.
Saving Tree Changes
ContentTreeResource atomically applies creates, deletes, and pointer-field updates in a single request:
let response = try await client.contentTree.save(TreeSaveRequest(
contentID: rootContentID,
creates: [
TreeNodeCreate(
clientID: UUID().uuidString,
datatypeID: datatypeID.rawValue,
parentID: rootContentID.rawValue
)
],
updates: [
TreeNodeUpdate(
contentDataID: existingNodeID,
firstChildID: "client-uuid-from-creates"
)
],
deletes: [removedNodeID]
))
print("Created: \(response.created), Updated: \(response.updated), Deleted: \(response.deleted)")
// Map client IDs to server-generated ULIDs
if let idMap = response.idMap {
for (clientID, serverID) in idMap {
print("\(clientID) -> \(serverID)")
}
}
// Check for partial failures
if let errors = response.errors, !errors.isEmpty {
for err in errors {
print("Error: \(err)")
}
}
Key types:
TreeNodeCreate-- new node with aclientIDyou generate. The server returns a mapping inidMap. Pointer fields can reference other client IDs.TreeNodeUpdate-- updates only the four pointer fields on an existing node. All other fields are preserved.TreeSaveRequest-- wraps creates, updates, and deletes.contentIDidentifies the root node for route inheritance.TreeSaveResponse-- counts of successful operations, theidMap, and any per-nodeerrors.
Good to know: The server processes deletes before updates, so removed nodes don't interfere with tree restructuring.
Composite Content Operations
ContentCompositeResource provides higher-level operations:
// Create a content node with field values in one request
let result = try await client.contentComposite.createWithFields(params: ContentCreateParams(
datatypeID: DatatypeID("01HXYZ..."),
fields: [
ContentCreateFieldValue(fieldID: FieldID("01HABC..."), value: "Hello World"),
ContentCreateFieldValue(fieldID: FieldID("01HDEF..."), value: "Post body text"),
]
))
print("Created content: \(result.contentData.contentDataID)")
// Delete a content node and all descendants
let deleteResult = try await client.contentComposite.deleteRecursive(id: ContentID("01HXYZ..."))
print("Deleted \(deleteResult.totalDeleted) nodes")
Batch Updates
ContentBatchResource applies multiple content changes in a single request:
let responseData = try await client.contentBatch.update(request: yourBatchPayload)
The update method accepts any Encodable & Sendable request body and returns raw Data. Structure your batch payload according to the batch API contract.
Media Upload
MediaUploadResource handles multipart file uploads:
let imageData = try Data(contentsOf: imageURL)
let media = try await client.mediaUpload.upload(
data: imageData,
filename: "hero-image.jpg"
)
print("Uploaded: \(media.mediaID) at \(media.s3Key)")
Upload with Path
Organize uploaded files into S3 key prefixes:
let media = try await client.mediaUpload.upload(
data: imageData,
filename: "hero.jpg",
options: MediaUploadResource.UploadOptions(path: "blog/headers")
)
Good to know: When
pathis nil, the server organizes files by date (YYYY/M).
Media Composite Operations
Scan for references before deleting:
// Check which content fields reference a media item
let refs = try await client.mediaComposite.getReferences(id: MediaID("01HXYZ..."))
print("\(refs.references.count) content fields reference this media")
// Delete media and clean up all references
try await client.mediaComposite.deleteWithCleanup(id: MediaID("01HXYZ..."))
Admin Media
Admin media items are stored separately and power the admin panel UI. The API mirrors the public media resources:
// List and get admin media
let adminItems = try await client.adminMedia.list()
let item = try await client.adminMedia.get(id: AdminMediaID("01HXYZ..."))
// Upload a file to admin media
let uploaded = try await client.adminMediaUpload.upload(
data: imageData,
filename: "admin-logo.png"
)
// Admin media folders
let tree = try await client.adminMediaFolders.tree()
let folderMedia = try await client.adminMediaFolders.listMedia(
folderID: AdminMediaFolderID("01HXYZ..."),
params: PaginationParams(limit: 20, offset: 0)
)
Publishing
PublishingResource manages content lifecycle:
// Publish content (creates a version snapshot)
let result = try await client.publishing.publish(req: PublishRequest(
contentDataID: "01HXYZ..."
))
// Unpublish
let result = try await client.publishing.unpublish(req: PublishRequest(
contentDataID: "01HXYZ..."
))
// Schedule future publication
let result = try await client.publishing.schedule(req: ScheduleRequest(
contentDataID: "01HXYZ...",
publishAt: "2026-04-01T00:00:00Z"
))
Versions
// List version history
let versions = try await client.publishing.listVersions(contentDataID: "01HXYZ...")
// Get a specific version
let version = try await client.publishing.getVersion(versionID: "01HVER...")
// Manually create a version snapshot
let version = try await client.publishing.createVersion(req: CreateVersionRequest(
contentDataID: "01HXYZ..."
))
// Restore content to a previous version
let result = try await client.publishing.restore(req: RestoreRequest(
contentDataID: "01HXYZ...",
versionID: "01HVER..."
))
// Delete a version
try await client.publishing.deleteVersion(versionID: "01HVER...")
Good to know: Admin content uses the parallel
adminPublishingresource with identical methods and admin-prefixed types.
Authentication
// Login
let response = try await client.auth.login(params: LoginParams(
email: "user@example.com",
password: "password"
))
print("Token: \(response.token)")
// Get current user
let user = try await client.auth.me()
// Register
let newUser = try await client.auth.register(params: CreateUserParams(
email: "new@example.com",
displayName: "New User",
password: "secure-password",
roleID: RoleID("01HROLE...")
))
// Password reset flow
let msg = try await client.auth.requestPasswordReset(params: RequestPasswordResetParams(
email: "user@example.com"
))
let confirm = try await client.auth.confirmPasswordReset(params: ConfirmPasswordResetParams(
token: "reset-token",
password: "new-password"
))
// Logout
try await client.auth.logout()
SwiftUI Integration
Observable ViewModel
import SwiftUI
import Modula
@Observable
final class ContentViewModel {
var datatypes: [Datatype] = []
var isLoading = false
var error: Error?
private let client: ModulaClient
init(client: ModulaClient) {
self.client = client
}
func loadDatatypes() async {
isLoading = true
defer { isLoading = false }
do {
datatypes = try await client.datatypes.list()
error = nil
} catch {
self.error = error
}
}
}
View with Task Loading
struct DatatypeListView: View {
let viewModel: ContentViewModel
var body: some View {
List(viewModel.datatypes, id: \.datatypeID) { dt in
VStack(alignment: .leading) {
Text(dt.label)
.font(.headline)
Text(dt.name)
.font(.caption)
.foregroundStyle(.secondary)
}
}
.overlay {
if viewModel.isLoading {
ProgressView()
}
}
.task {
await viewModel.loadDatatypes()
}
}
}
Paginated Loading
@Observable
final class PaginatedViewModel<T: Decodable & Sendable> {
var items: [T] = []
var total: Int64 = 0
var isLoading = false
private var offset: Int64 = 0
private let pageSize: Int64 = 20
func loadPage<ID: ResourceID>(
from resource: Resource<T, some Encodable & Sendable, some Encodable & Sendable, ID>
) async throws {
isLoading = true
defer { isLoading = false }
let page = try await resource.listPaginated(params: PaginationParams(
limit: pageSize,
offset: offset
))
items.append(contentsOf: page.data)
total = page.total
offset += pageSize
}
var hasMore: Bool { Int64(items.count) < total }
}
Next Steps
- Error Handling -- APIError, do/catch patterns, network vs API errors
- Reference -- all resources, ID types, enums, JSONValue