Swift Integration
Add content moderation to your iOS app using Vettly's REST API with URLSession.
Using React Native or Expo? See the React Native or Expo guides instead.
Quick Start
swift
import Foundation
struct ModerationResult: Codable {
let flagged: Bool
let action: String
let categories: [String: Double]
let policy: String
let latencyMs: Int
enum CodingKeys: String, CodingKey {
case flagged, action, categories, policy
case latencyMs = "latency_ms"
}
}
class VettlyClient {
private let apiKey: String
private let baseURL = "https://api.vettly.dev/v1"
init(apiKey: String) {
self.apiKey = apiKey
}
func check(content: String, contentType: String = "text", policyId: String? = nil) async throws -> ModerationResult {
let url = URL(string: "\(baseURL)/check")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
var body: [String: Any] = [
"content": content,
"contentType": contentType
]
if let policyId {
body["policyId"] = policyId
}
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw VettlyError.invalidResponse
}
if httpResponse.statusCode == 429 {
throw VettlyError.rateLimited
}
if httpResponse.statusCode != 200 {
throw VettlyError.apiError(statusCode: httpResponse.statusCode)
}
return try JSONDecoder().decode(ModerationResult.self, from: data)
}
}
enum VettlyError: Error {
case invalidResponse
case rateLimited
case apiError(statusCode: Int)
}Usage:
swift
let vettly = VettlyClient(apiKey: "vettly_your_api_key")
let result = try await vettly.check(content: "Hello, world!")
if result.action == "block" {
// Content is not allowed
} else {
// Content is safe to post
}Text Moderation
Check User Comments Before Posting
swift
func submitComment(_ text: String) async throws {
let result = try await vettly.check(content: text)
switch result.action {
case "block":
throw CommentError.blocked("This comment is not allowed.")
case "flag":
// Queue for human review, but allow posting
try await postComment(text, needsReview: true)
default:
try await postComment(text, needsReview: false)
}
}Chat Message Moderation
swift
class ChatViewModel: ObservableObject {
@Published var messages: [ChatMessage] = []
@Published var error: String?
@Published var isSending = false
private let vettly = VettlyClient(apiKey: apiKey)
func send(_ text: String) async {
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
await MainActor.run { isSending = true }
do {
let result = try await vettly.check(content: text)
if result.action == "block" {
await MainActor.run {
error = "Message contains inappropriate content"
isSending = false
}
return
}
// Send the message
let message = ChatMessage(text: text, sender: .me)
await MainActor.run {
messages.append(message)
error = nil
isSending = false
}
} catch {
await MainActor.run {
self.error = "Failed to send message"
isSending = false
}
}
}
}Image Moderation
Check Camera or Gallery Uploads
swift
import PhotosUI
import SwiftUI
func moderateImage(_ imageData: Data) async throws -> Bool {
let base64 = imageData.base64EncodedString()
let result = try await vettly.check(
content: base64,
contentType: "image"
)
return result.action != "block"
}With PhotosPicker (SwiftUI)
swift
struct ImageUploadView: View {
@State private var selectedItem: PhotosPickerItem?
@State private var isChecking = false
@State private var error: String?
var body: some View {
VStack {
PhotosPicker("Select Photo", selection: $selectedItem, matching: .images)
if isChecking {
ProgressView("Checking image...")
}
if let error {
Text(error).foregroundColor(.red)
}
}
.onChange(of: selectedItem) { _, newItem in
Task {
guard let data = try? await newItem?.loadTransferable(type: Data.self) else { return }
isChecking = true
error = nil
do {
let allowed = try await moderateImage(data)
if !allowed {
error = "This image is not allowed"
} else {
// Proceed with upload
}
} catch {
self.error = "Failed to check image"
}
isChecking = false
}
}
}
}Report Button
Let users report offensive content via the appeals API.
swift
func reportContent(contentId: String, reason: String) async throws {
let url = URL(string: "https://api.vettly.dev/v1/appeals")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"contentId": contentId,
"reason": reason,
"reporterId": currentUserId
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (_, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 201 else {
throw ReportError.failed
}
}SwiftUI Report Button
swift
struct ReportButton: View {
let contentId: String
@State private var showSheet = false
@State private var reason = ""
@State private var isSubmitting = false
var body: some View {
Button("Report") { showSheet = true }
.foregroundColor(.red)
.sheet(isPresented: $showSheet) {
NavigationView {
Form {
Section("Why are you reporting this?") {
TextEditor(text: $reason)
.frame(minHeight: 100)
}
Button(isSubmitting ? "Submitting..." : "Submit Report") {
Task {
isSubmitting = true
try? await reportContent(contentId: contentId, reason: reason)
isSubmitting = false
showSheet = false
}
}
.disabled(reason.isEmpty || isSubmitting)
}
.navigationTitle("Report Content")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { showSheet = false }
}
}
}
}
}
}Block User
Add users to a blocklist via the API.
swift
func blockUser(_ blockedUserId: String) async throws {
let url = URL(string: "https://api.vettly.dev/v1/blocklist")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"userId": currentUserId,
"blockedUserId": blockedUserId
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (_, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 201 else {
throw BlockError.failed
}
}SwiftUI Block Button
swift
struct BlockUserButton: View {
let userId: String
@State private var showConfirmation = false
var body: some View {
Button("Block User") { showConfirmation = true }
.foregroundColor(.red)
.alert("Block User?", isPresented: $showConfirmation) {
Button("Block", role: .destructive) {
Task {
try? await blockUser(userId)
}
}
Button("Cancel", role: .cancel) {}
} message: {
Text("You will no longer see content from this user.")
}
}
}Error Handling
swift
func safeModerate(_ content: String) async -> ModerationResult? {
do {
return try await vettly.check(content: content)
} catch VettlyError.rateLimited {
// Back off and retry after delay
try? await Task.sleep(nanoseconds: 1_000_000_000)
return try? await vettly.check(content: content)
} catch VettlyError.apiError(let statusCode) {
print("Vettly API error: \(statusCode)")
return nil // Fail open or closed based on your policy
} catch {
print("Network error: \(error)")
return nil
}
}Server-Side (Recommended for Production)
For production apps, moderate on your backend to keep your API key secure:
swift
// iOS app — call your own backend
func sendMessage(_ content: String) async throws {
let url = URL(string: "https://your-api.com/messages")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(userToken)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = ["content": content]
request.httpBody = try JSONEncoder().encode(body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else { return }
if httpResponse.statusCode == 403 {
throw MessageError.blocked
}
}typescript
// Your backend (Node.js / Express)
app.post('/messages', async (req, res) => {
const { content } = req.body
const result = await vettly.check({ content, contentType: 'text' })
if (result.action === 'block') {
return res.status(403).json({ error: 'Content not allowed' })
}
await db.messages.create({ content, userId: req.user.id })
res.json({ success: true })
})Configuration
Store your API key securely. Never hardcode it in client-side code for production apps.
swift
// Use Xcode configuration or a secrets manager
enum Config {
static let vettlyApiKey: String = {
guard let key = Bundle.main.infoDictionary?["VETTLY_API_KEY"] as? String else {
fatalError("VETTLY_API_KEY not set in Info.plist")
}
return key
}()
}
let vettly = VettlyClient(apiKey: Config.vettlyApiKey)Next Steps
- App Store Compliance — Full Guideline 1.2 compliance guide
- Custom Policies — Configure moderation rules
- Best Practices — Mobile moderation patterns
