CMS for Swift
Noma gives Swift teams a secure API-first content workflow for iOS apps, with backend-safe CMS access, localized delivery, and release-safe publishing without waiting for binary deployments.
Why Swift teams use Noma
iOS teams need reliable APIs, strict transport security, and safe content rollout controls. Noma provides structured content, localization, and publish/version workflows while your Swift app focuses on native UX and performance.
This guide is aligned with current Apple documentation for URLSession, ATS, Keychain, BackgroundTasks, and Codable data modeling.
Use a backend boundary for CMS access
Do not embed privileged CMS API keys in iOS binaries. Put Noma access in your backend and expose iOS-safe endpoints with scoped payloads.
// backend/lib/noma.ts
import { createClient } from "@nomacms/js-sdk";
export function getNomaServerClient() {
const projectId = process.env.NOMA_PROJECT_ID;
const apiKey = process.env.NOMA_API_KEY;
if (!projectId || !apiKey) throw new Error("Missing Noma env vars");
return createClient({ projectId, apiKey });
}URLSession + Codable over ATS-secure transport
Apple recommends high-level networking APIs and ATS-secure connections. Use URLSession and Codable to parse backend JSON into typed Swift models.
import Foundation
struct FeedItem: Codable {
let uuid: String
let title: String
let summary: String?
}
struct FeedResponse: Codable {
let items: [FeedItem]
}
final class MobileAPIClient {
private let session: URLSession
private let baseURL = URL(string: "https://api.example.com")!
init(session: URLSession = .shared) {
self.session = session
}
func fetchHomeFeed(locale: String) async throws -> [FeedItem] {
var components = URLComponents(
url: baseURL.appendingPathComponent("/mobile/home-feed"),
resolvingAgainstBaseURL: false
)!
components.queryItems = [URLQueryItem(name: "locale", value: locale)]
let request = URLRequest(url: components.url!)
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse, 200..<300 ~= http.statusCode else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(FeedResponse.self, from: data).items
}
}Keep ATS enabled by default and prefer fixing server TLS configuration rather than broad NSAllowsArbitraryLoads exceptions.
Store tokens in Keychain
Keychain services are the default secure storage path for secrets on Apple platforms. Keep auth/session secrets out of UserDefaults.
import Foundation
import Security
enum TokenStore {
static func save(token: String, account: String = "noma_access_token") throws {
let data = Data(token.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw URLError(.cannotCreateFile) }
}
}Repository pattern for fast app startup
Use local-first reads and background refresh for resilient mobile UX under variable network conditions. Noma list/get patterns map cleanly to this repository model.
import Foundation
final class HomeFeedRepository {
private let api: MobileAPIClient
private let cache: FeedCache
init(api: MobileAPIClient, cache: FeedCache) {
self.api = api
self.cache = cache
}
// Local-first read, then async refresh.
func get(locale: String) async throws -> [FeedItem] {
let cached = try cache.read(locale: locale)
if !cached.isEmpty {
Task { try? await refresh(locale: locale) }
return cached
}
return try await refresh(locale: locale)
}
func refresh(locale: String) async throws -> [FeedItem] {
let fresh = try await api.fetchHomeFeed(locale: locale)
try cache.write(fresh, locale: locale)
return fresh
}
}Use BackgroundTasks for refresh workflows
For periodic content refresh and maintenance, schedule background work with BackgroundTasks and update local cache when tasks run.
// App startup:
// BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.refresh", using: nil) { task in ... }
//
// Schedule refresh request with BGAppRefreshTaskRequest and
// perform API sync + cache update when system launches task.Publish and rollback without app redeploys
Content for banners, onboarding, and pricing can be updated through Noma draft/publish/version controls independently of App Store release timing.
// backend/content-release.ts
const noma = getNomaServerClient();
await noma.content.patch("ios_config", configUuid, {
data: { promo_banner: "Summer campaign" },
});
await noma.content.publish("ios_config", configUuid);
const versions = await noma.content.versions.list("ios_config", configUuid);
await noma.content.versions.revert("ios_config", configUuid, versions[0].uuid);Swift mobile content ops with MCP
Use @nomacms/mcp-server and workflows from Agent Skills to automate schema updates, localization workflows, and release QA checks.