```html How to Build a Kanban Board App in SwiftUI (2026)

How to Build a Kanban Board App in SwiftUI

A Kanban Board app lets users organize tasks across visual columns — To Do, In Progress, Done — using drag-and-drop cards. It's ideal for indie developers targeting productivity-focused iOS users who want a lightweight, local-first project tracker.

iOS 17+ · Xcode 16+ · SwiftUI Complexity: Intermediate Estimated time: ~1 week Updated: May 12, 2026

Prerequisites

Architecture overview

The app uses SwiftData as the single source of truth, with three @Model types: Board, KanbanColumn, and Card. Views are driven entirely by @Query and @Bindable — no separate view-model layer needed at this complexity. Drag-and-drop state is managed transiently in @State on each column view; on drop confirmation the SwiftData context is mutated and persists automatically. StoreKit 2 handles the one-time unlock check at app launch.

KanbanApp/
├── Models/
│   ├── Board.swift          # @Model: title, columns
│   ├── KanbanColumn.swift   # @Model: title, order, cards
│   └── Card.swift           # @Model: title, notes, order, dueDate
├── Views/
│   ├── BoardView.swift      # HScrollView of columns
│   ├── ColumnView.swift     # LazyVStack + drop target
│   └── CardView.swift       # draggable card tile
├── Store/
│   └── PurchaseManager.swift
└── PrivacyInfo.xcprivacy

Step-by-step

1. Data model with SwiftData

Define the three @Model classes with ordered, cascade-delete relationships so columns and cards persist and sort correctly across launches.

import SwiftData
import Foundation

@Model
final class Board {
    var title: String
    @Relationship(deleteRule: .cascade) var columns: [KanbanColumn] = []
    init(title: String) { self.title = title }
}

@Model
final class KanbanColumn {
    var title: String
    var order: Int
    @Relationship(deleteRule: .cascade) var cards: [Card] = []
    init(title: String, order: Int) {
        self.title = title
        self.order = order
    }
}

@Model
final class Card {
    var title: String
    var notes: String
    var order: Int
    var dueDate: Date?
    var createdAt: Date
    init(title: String, order: Int) {
        self.title = title
        self.notes = ""
        self.order = order
        self.createdAt = .now
    }
}

2. Core board UI with horizontal column layout

Render the board as a horizontal ScrollView of fixed-width column tiles, each querying its cards sorted by order.

struct BoardView: View {
    @Bindable var board: Board
    @Environment(\.modelContext) private var context

    var sortedColumns: [KanbanColumn] {
        board.columns.sorted { $0.order < $1.order }
    }

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(alignment: .top, spacing: 16) {
                ForEach(sortedColumns) { column in
                    ColumnView(column: column)
                        .frame(width: 260)
                }
                Button {
                    let col = KanbanColumn(
                        title: "New Column",
                        order: board.columns.count
                    )
                    board.columns.append(col)
                } label: {
                    Label("Add Column", systemImage: "plus")
                        .frame(width: 200, height: 44)
                        .background(.secondary.opacity(0.15))
                        .clipShape(RoundedRectangle(cornerRadius: 10))
                }
            }
            .padding()
        }
        .navigationTitle(board.title)
    }
}

3. Drag-and-drop card management

Use .draggable on each CardView and .dropDestination on each column to move cards between columns, re-assigning parent relationships in the SwiftData context.

struct ColumnView: View {
    @Bindable var column: KanbanColumn
    @Environment(\.modelContext) private var context

    var sortedCards: [Card] {
        column.cards.sorted { $0.order < $1.order }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(column.title).font(.headline).padding(.horizontal, 8)
            LazyVStack(spacing: 8) {
                ForEach(sortedCards) { card in
                    CardView(card: card)
                        .draggable(card.persistentModelID.hashValue.description)
                }
            }
            .dropDestination(for: String.self) { items, _ in
                guard let idStr = items.first,
                      let idVal = Int(idStr) else { return false }
                // Locate card by matching hashValue and move it
                let all = try? context.fetch(FetchDescriptor())
                if let card = all?.first(where: {
                    $0.persistentModelID.hashValue == idVal
                }) {
                    card.order = column.cards.count
                    column.cards.append(card)
                }
                return true
            }
            .padding(6)
            .background(.secondary.opacity(0.08))
            .clipShape(RoundedRectangle(cornerRadius: 12))
        }
    }
}

4. Privacy Manifest setup

Apple requires a PrivacyInfo.xcprivacy file for any app using certain system APIs — add it before submitting to avoid automatic rejection.

<!-- PrivacyInfo.xcprivacy (XML property list) -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>NSPrivacyTracking</key>  <false/>
  <key>NSPrivacyTrackingDomains</key>  <array/>
  <key>NSPrivacyCollectedDataTypes</key>  <array/>
  <key>NSPrivacyAccessedAPITypes</key>
  <array>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array><string>CA92.1</string></array>
    </dict>
  </array>
</dict>
</plist>

Common pitfalls

Adding monetization: One-time purchase

Implement a one-time unlock using StoreKit 2's Product.purchase() API. Gate features like unlimited boards (free tier can have 1) or card attachments behind a single non-consumable IAP. At app launch, call Transaction.currentEntitlement(for:) to check purchase status and store the result in a @Published property on a lightweight PurchaseManager ObservableObject. Avoid restoring purchases manually — StoreKit 2 handles restoration automatically on reinstall via Transaction.currentEntitlement. Set the IAP to "No Ads" or "Pro Unlock" in App Store Connect and price it in the $2.99–$4.99 tier; one-time purchases on productivity tools convert well when the free tier is genuinely useful but visibly limited.

Shipping this faster with Soarias

Soarias scaffolds the full SwiftData model layer and column/card view hierarchy from a prompt, generates your PrivacyInfo.xcprivacy with the correct reason codes already filled in, wires up fastlane with match for code signing, and handles App Store Connect metadata — app description, keywords, screenshots at all required device sizes — and submits the binary. For a Kanban Board app that means the setup that typically costs half your first weekend (project structure, signing, ASC config) is done before you write your first line of drag-and-drop logic.

At intermediate complexity with roughly a week of part-time build time, Soarias realistically compresses the non-coding overhead — provisioning, screenshot production, ASC form-filling, fastlane config — from 4–6 hours spread across the week down to under 30 minutes. You spend the week on the actual product, not the pipeline.

Related guides

FAQ

Do I need a paid Apple Developer account?

Yes. The free Apple ID lets you sideload onto your own device via Xcode, but TestFlight distribution and App Store submission both require an active Apple Developer Program membership at $99/year.

How do I submit this to the App Store?

Archive the app in Xcode (Product → Archive), upload via the Organizer or altool/fastlane, then complete the App Store Connect listing — screenshots, description, privacy labels, and pricing — before clicking "Submit for Review." First-time reviews typically take 1–2 business days.

Can I sync boards across devices with iCloud?

Yes — add the iCloud capability in Xcode, enable CloudKit in App Store Connect, and swap ModelContainer(for:) for a configuration that includes cloudKitDatabase: .automatic. Note that CloudKit sync requires your @Model classes to have all optional properties or default values, since CloudKit cannot store non-optional nil-less fields.

Last reviewed: 2026-05-12 by the Soarias team.

```