Skip to content

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

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
    }
}

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