> ## Documentation Index
> Fetch the complete documentation index at: https://buttercms.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Swift

> Build content-driven iOS applications with ButterCMS. Covers setup, fetching pages, collections, components, and blog content using the official Swift SDK.

## 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](../../core-concepts/content-types/pages), [Collections](../../core-concepts/content-types/collections), and [Blog Posts](../../core-concepts/content-types/blog-engine).

<Tip>
  In order for the snippets to work, you'll need to [setup your dashboard content schemas inside of ButterCMS](../buttercms-setup) first.
</Tip>

## Installation

### Swift Package Manager (recommended)

Add the package through Xcode:

1. Open Xcode → **File** → **Add 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`:

```ruby theme={null}
target 'YourAppName' do
  use_frameworks!
  pod 'ButterCMSSDK', '~> 1.0.7'
end
```

Then install:

```bash theme={null}
pod install
```

### Configuration

Add your API token to your app's configuration:

```swift theme={null}
// 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:

```swift theme={null}
import ButterCMSSDK

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

## Basic usage

### Fetch a Page

```swift theme={null}
// 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

```swift theme={null}
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

<Tabs>
  <Tab title="Completion Handler">
    ```swift theme={null}
    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
        }
    ```
  </Tab>

  <Tab title="Completion Handlers (iOS 13+)">
    ```swift theme={null}
    // ButterCMS.swift
    import Foundation

    class ButterCMS {
        static let shared = ButterCMS()

        private let baseURL = "https://api.buttercms.com/v2"
        private let apiToken: String

        private init() {
            self.apiToken = Config.butterCMSToken
        }

        private func request<T: Decodable>(
            endpoint: String,
            queryItems: [URLQueryItem] = [],
            completion: @escaping (Result<T, Error>) -> Void
        ) {
            var components = URLComponents(string: "\(baseURL)/\(endpoint)")!
            var items = queryItems
            items.append(URLQueryItem(name: "auth_token", value: apiToken))
            components.queryItems = items

            URLSession.shared.dataTask(with: components.url!) { data, response, error in
                if let error = error {
                    completion(.failure(error))
                    return
                }

                guard let data = data else {
                    completion(.failure(ButterCMSError.requestFailed))
                    return
                }

                do {
                    let decoder = JSONDecoder()
                    decoder.keyDecodingStrategy = .convertFromSnakeCase
                    let result = try decoder.decode(T.self, from: data)
                    completion(.success(result))
                } catch {
                    completion(.failure(error))
                }
            }.resume()
        }

        func getPage(pageType: String, slug: String, completion: @escaping (Result<PageResponse, Error>) -> Void) {
            request(endpoint: "pages/\(pageType)/\(slug)/", completion: completion)
        }

        func getPosts(page: Int = 1, pageSize: Int = 10, completion: @escaping (Result<PostsResponse, Error>) -> Void) {
            let queryItems = [
                URLQueryItem(name: "page", value: String(page)),
                URLQueryItem(name: "page_size", value: String(pageSize))
            ]
            request(endpoint: "posts/", queryItems: queryItems, completion: completion)
        }
    }
    ```
  </Tab>
</Tabs>

<Info>
  For complete API documentation including all available endpoints and parameters, see the [REST API Reference](../../api-reference/pages/get-multiple-pages).
</Info>

## Models

```swift theme={null}
// 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

<Tabs>
  <Tab title="SwiftUI">
    ```swift theme={null}
    // 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
        }
    }
    ```
  </Tab>

  <Tab title="UIKit">
    ```swift theme={null}
    // LandingPageViewController.swift
    import UIKit

    class LandingPageViewController: UIViewController {
        private let slug: String
        private let scrollView = UIScrollView()
        private let contentStack = UIStackView()

        init(slug: String) {
            self.slug = slug
            super.init(nibName: nil, bundle: nil)
        }

        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        override func viewDidLoad() {
            super.viewDidLoad()
            setupUI()
            loadPage()
        }

        private func setupUI() {
            view.backgroundColor = .systemBackground
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            contentStack.translatesAutoresizingMaskIntoConstraints = false
            contentStack.axis = .vertical
            contentStack.spacing = 16

            view.addSubview(scrollView)
            scrollView.addSubview(contentStack)

            NSLayoutConstraint.activate([
                scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
                scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
                contentStack.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16),
                contentStack.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16),
                contentStack.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -16),
                contentStack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
                contentStack.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -32)
            ])
        }

        private func loadPage() {
            Task {
                do {
                    let response = try await ButterCMS.shared.getPage(pageType: "landing_page", slug: slug)
                    await MainActor.run {
                        displayPage(response.data)
                    }
                } catch {
                    print("Error: \(error)")
                }
            }
        }

        private func displayPage(_ page: PageData) {
            if let headline = page.fields["headline"]?.value as? String {
                let label = UILabel()
                label.text = headline
                label.font = .preferredFont(forTextStyle: .largeTitle)
                label.numberOfLines = 0
                contentStack.addArrangedSubview(label)
            }
            // Add more fields...
        }
    }
    ```
  </Tab>
</Tabs>

## Collections

```swift theme={null}
// 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

```swift theme={null}
// 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

```swift theme={null}
// 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

<Tabs>
  <Tab title="Blog List">
    ```swift theme={null}
    // 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)")
            }
        }
    }
    ```
  </Tab>

  <Tab title="Single Blog Post">
    ```swift theme={null}
    // BlogPostView.swift
    import SwiftUI

    struct BlogPostView: View {
        let slug: String
        @State private var post: Post?
        @State private var isLoading = true

        var body: some View {
            Group {
                if isLoading {
                    ProgressView()
                } else if let post = post {
                    ScrollView {
                        VStack(alignment: .leading, spacing: 16) {
                            Text(post.title)
                                .font(.title)
                                .fontWeight(.bold)

                            HStack {
                                Text("By \(post.author.firstName) \(post.author.lastName)")
                                Spacer()
                                Text(formatDate(post.published))
                            }
                            .font(.caption)
                            .foregroundColor(.secondary)

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

                            HTMLTextView(html: post.body)
                        }
                        .padding()
                    }
                }
            }
            .navigationBarTitleDisplayMode(.inline)
            .task {
                await loadPost()
            }
        }

        private func loadPost() async {
            do {
                let response = try await ButterCMS.shared.getPost(slug: slug)
                post = response.data
            } catch {
                print("Error: \(error)")
            }
            isLoading = false
        }

        private func formatDate(_ dateString: String) -> String {
            let formatter = ISO8601DateFormatter()
            guard let date = formatter.date(from: dateString) else { return dateString }

            let displayFormatter = DateFormatter()
            displayFormatter.dateStyle = .medium
            return displayFormatter.string(from: date)
        }
    }
    ```
  </Tab>
</Tabs>

## Caching

Use `URLCache` for network caching:

```swift theme={null}
// 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

<CardGroup cols={2}>
  <Card title="Swift SDK" icon="swift" href="../sdks/swift-sdk">
    Swift SDK reference
  </Card>

  <Card title="Android Guide" icon="android" href="./android">
    Android integration
  </Card>

  <Card title="GitHub Repository" icon="github" href="https://github.com/ButterCMS">
    View source code
  </Card>

  <Card title="Content API" icon="database" href="../../api-reference/pages/get-multiple-pages">
    REST API documentation
  </Card>
</CardGroup>
