Skip to main content

Overview

This integration guide shows you how to how to update your existing project to:
  1. install the ButterCMS package
  2. instantiate ButterCMS
  3. create components to fetch and display each of the three ButterCMS content types: Pages, Collections, and Blog Posts.
In order for the snippets to work, you’ll need to setup your dashboard content schemas inside of ButterCMS first.

Installation

Add the package through Xcode:
  1. Open Xcode → FileAdd Package
  2. Search for buttercms-swift
  3. Select the package and adjust dependency version if needed
  4. Click Add Package

CocoaPods

Add to your Podfile:
target 'YourAppName' do
  use_frameworks!
  pod 'ButterCMSSDK', '~> 1.0.7'
end
Then install:
pod install

Configuration

Add your API token to your app’s configuration:
// Config.swift
enum Config {
    static let butterCMSToken = ProcessInfo.processInfo.environment["BUTTERCMS_API_TOKEN"] ?? "your_api_token"
}

Initialize the client

Import and initialize the ButterCMS SDK:
import ButterCMSSDK

// Initialize with your API token
let butterClient = ButterCMSClient(apiKey: Config.butterCMSToken)

Basic usage

Fetch a Page

// Async/Await (iOS 15+)
do {
    let response = try await butterClient.getPage(pageType: "landing-page", pageSlug: "home")
    let fields = response.data.fields
    print("Headline: \(fields["headline"])")
} catch {
    print("Error fetching page: \(error)")
}

// Completion Handler
butterClient.getPage(pageType: "landing-page", pageSlug: "home") { result in
    switch result {
    case .success(let response):
        let fields = response.data.fields
        print("Headline: \(fields["headline"])")
    case .failure(let error):
        print("Error: \(error)")
    }
}

Fetch collection items

do {
    let response = try await butterClient.getCollection(collectionSlug: "brands")
    let items = response.data
    for item in items {
        print("Brand: \(item.fields["name"])")
    }
} catch {
    print("Error fetching collection: \(error)")
}

Fetch Blog Posts

do {
    let response = try await butterClient.getPosts(page: 1, pageSize: 10)
    let posts = response.data
    for post in posts {
        print("Title: \(post.title)")
        print("Author: \(post.author.firstName) \(post.author.lastName)")
    }
} catch {
    print("Error fetching posts: \(error)
                URLQueryItem(name: "page", value: String(page)),
                URLQueryItem(name: "page_size", value: String(pageSize))
            ]
            return try await request(endpoint: "posts/", queryItems: queryItems)
        }

        func getPost(slug: String) async throws -> PostResponse {
            return try await request(endpoint: "posts/\(slug)/")
        }
    }

    enum ButterCMSError: Error {
        case requestFailed
        case notFound
    }
For complete API documentation including all available endpoints and parameters, see the REST API Reference.

Models

// Models.swift
import Foundation

struct PageResponse: Codable {
    let data: PageData
}

struct PageData: Codable {
    let slug: String
    let fields: [String: AnyCodable]
}

struct CollectionResponse: Codable {
    let data: [String: [AnyCodable]]
}

struct PostsResponse: Codable {
    let data: [Post]
    let meta: Meta
}

struct PostResponse: Codable {
    let data: Post
}

struct Post: Codable, Identifiable {
    var id: String { slug }
    let slug: String
    let title: String
    let body: String
    let summary: String
    let published: String
    let featuredImage: String?
    let author: Author
    let categories: [Category]
    let tags: [Tag]
}

struct Author: Codable {
    let firstName: String
    let lastName: String
    let email: String?
    let bio: String?
}

struct Category: Codable {
    let name: String
    let slug: String
}

struct Tag: Codable {
    let name: String
    let slug: String
}

struct Meta: Codable {
    let nextPage: Int?
    let previousPage: Int?
    let count: Int
}

// AnyCodable helper for dynamic content
struct AnyCodable: Codable {
    let value: Any

    init(_ value: Any) {
        self.value = value
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let string = try? container.decode(String.self) {
            value = string
        } else if let int = try? container.decode(Int.self) {
            value = int
        } else if let double = try? container.decode(Double.self) {
            value = double
        } else if let bool = try? container.decode(Bool.self) {
            value = bool
        } else if let array = try? container.decode([AnyCodable].self) {
            value = array.map { $0.value }
        } else if let dict = try? container.decode([String: AnyCodable].self) {
            value = dict.mapValues { $0.value }
        } else {
            value = NSNull()
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        if let string = value as? String {
            try container.encode(string)
        } else if let int = value as? Int {
            try container.encode(int)
        } else if let double = value as? Double {
            try container.encode(double)
        } else if let bool = value as? Bool {
            try container.encode(bool)
        } else {
            try container.encodeNil()
        }
    }
}

Pages

// LandingPageView.swift
import SwiftUI

struct LandingPageView: View {
    let slug: String
    @State private var page: PageData?
    @State private var isLoading = true
    @State private var error: Error?

    var body: some View {
        Group {
            if isLoading {
                ProgressView()
            } else if let error = error {
                VStack {
                    Text("Error loading page")
                    Text(error.localizedDescription)
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            } else if let page = page {
                ScrollView {
                    VStack(alignment: .leading, spacing: 16) {
                        if let headline = page.fields["headline"]?.value as? String {
                            Text(headline)
                                .font(.largeTitle)
                                .fontWeight(.bold)
                        }

                        if let subheadline = page.fields["subheadline"]?.value as? String {
                            Text(subheadline)
                                .font(.subheadline)
                                .foregroundColor(.secondary)
                        }

                        if let imageURL = page.fields["hero_image"]?.value as? String,
                           let url = URL(string: imageURL) {
                            AsyncImage(url: url) { image in
                                image
                                    .resizable()
                                    .aspectRatio(contentMode: .fit)
                                    .cornerRadius(8)
                            } placeholder: {
                                ProgressView()
                            }
                        }

                        if let body = page.fields["body"]?.value as? String {
                            HTMLTextView(html: body)
                        }
                    }
                    .padding()
                }
            }
        }
        .task {
            await loadPage()
        }
    }

    private func loadPage() async {
        do {
            let response = try await ButterCMS.shared.getPage(pageType: "landing-page", slug: slug)
            page = response.data
        } catch {
            self.error = error
        }
        isLoading = false
    }
}

Collections

// BrandsView.swift
import SwiftUI

struct BrandsView: View {
    @State private var brands: [[String: Any]] = []
    @State private var isLoading = true

    var body: some View {
        NavigationView {
            Group {
                if isLoading {
                    ProgressView()
                } else {
                    List(brands.indices, id: \.self) { index in
                        let brand = brands[index]
                        HStack {
                            if let logoURL = brand["logo"] as? String,
                               let url = URL(string: logoURL) {
                                AsyncImage(url: url) { image in
                                    image.resizable().frame(width: 50, height: 50)
                                } placeholder: {
                                    ProgressView()
                                }
                            }
                            Text(brand["name"] as? String ?? "")
                                .font(.headline)
                        }
                    }
                }
            }
            .navigationTitle("Our Brands")
            .task {
                await loadBrands()
            }
        }
    }

    private func loadBrands() async {
        do {
            let response = try await ButterCMS.shared.getCollection(keys: ["brands"])
            if let brandsData = response.data["brands"] {
                brands = brandsData.map { $0.value as? [String: Any] ?? [:] }
            }
        } catch {
            print("Error: \(error)")
        }
        isLoading = false
    }
}

Dynamic components

Component views

// Components/HeroView.swift
import SwiftUI

struct HeroComponentView: View {
    let headline: String
    let subheadline: String
    let image: String?
    let buttonLabel: String?
    let buttonUrl: String?

    var body: some View {
        VStack(spacing: 16) {
            Text(headline)
                .font(.title)
                .fontWeight(.bold)

            Text(subheadline)
                .font(.body)
                .foregroundColor(.secondary)

            if let buttonLabel = buttonLabel {
                Button(buttonLabel) {
                    // Handle navigation
                }
                .buttonStyle(.borderedProminent)
            }

            if let imageURL = image, let url = URL(string: imageURL) {
                AsyncImage(url: url) { image in
                    image.resizable().aspectRatio(contentMode: .fit)
                } placeholder: {
                    ProgressView()
                }
            }
        }
        .padding()
    }
}

Component renderer

// ComponentRenderer.swift
import SwiftUI

struct ComponentRenderer: View {
    let components: [[String: Any]]

    var body: some View {
        ForEach(components.indices, id: \.self) { index in
            renderComponent(components[index])
        }
    }

    @ViewBuilder
    private func renderComponent(_ component: [String: Any]) -> some View {
        let type = component["type"] as? String ?? ""
        let fields = component["fields"] as? [String: Any] ?? [:]

        switch type {
        case "hero":
            HeroComponentView(
                headline: fields["headline"] as? String ?? "",
                subheadline: fields["subheadline"] as? String ?? "",
                image: fields["image"] as? String,
                buttonLabel: fields["button_label"] as? String,
                buttonUrl: fields["button_url"] as? String
            )
        default:
            EmptyView()
        }
    }
}

Blog

// BlogListView.swift
import SwiftUI

struct BlogListView: View {
    @State private var posts: [Post] = []
    @State private var isLoading = true
    @State private var currentPage = 1
    @State private var hasNextPage = true

    var body: some View {
        NavigationView {
            Group {
                if isLoading && posts.isEmpty {
                    ProgressView()
                } else {
                    List {
                        ForEach(posts) { post in
                            NavigationLink(destination: BlogPostView(slug: post.slug)) {
                                VStack(alignment: .leading, spacing: 4) {
                                    Text(post.title)
                                        .font(.headline)
                                    Text("By \(post.author.firstName) \(post.author.lastName)")
                                        .font(.caption)
                                        .foregroundColor(.secondary)
                                }
                                .padding(.vertical, 4)
                            }
                        }

                        if hasNextPage {
                            Button("Load More") {
                                Task { await loadMorePosts() }
                            }
                            .frame(maxWidth: .infinity)
                        }
                    }
                }
            }
            .navigationTitle("Blog")
            .task {
                await loadPosts()
            }
        }
    }

    private func loadPosts() async {
        do {
            let response = try await ButterCMS.shared.getPosts(page: currentPage)
            posts = response.data
            hasNextPage = response.meta.nextPage != nil
        } catch {
            print("Error: \(error)")
        }
        isLoading = false
    }

    private func loadMorePosts() async {
        currentPage += 1
        do {
            let response = try await ButterCMS.shared.getPosts(page: currentPage)
            posts.append(contentsOf: response.data)
            hasNextPage = response.meta.nextPage != nil
        } catch {
            print("Error: \(error)")
        }
    }
}

Caching

Use URLCache for network caching:
// CacheConfig.swift
import Foundation

func configureURLCache() {
    let cache = URLCache(
        memoryCapacity: 10 * 1024 * 1024,  // 10 MB memory
        diskCapacity: 50 * 1024 * 1024,     // 50 MB disk
        diskPath: "buttercms_cache"
    )
    URLCache.shared = cache
}

Resources

Swift SDK

Swift SDK reference

Android Guide

Android integration

GitHub Repository

View source code

Content API

REST API documentation