SwiftUI×Supabaseでアプリを作ろう

# はじめに

最近、**FirebaseのStorageが有料化** したりなど、個人開発でFirebaseを使うのがだんだんと躊躇われるようになってきました。

そこで、次世代Firebaseとも呼び声の高いSupabaseを使って、iPhoneアプリを作っていきます。

# Supabaseとは？

Supabaseは、オープンソースのBackend as a Service（BaaS）で、PostgreSQLを基盤としたリアルタイムデータベース、認証機能、ストレージ、サーバーレス関数などを提供します。これにより、開発者は複雑なバックエンドの構築を最小限に抑えつつ、高速かつ安全にアプリケーションを開発できます。また、リアルタイム同期やロールベースのアクセス制御などの高度な機能が特徴で、Firebaseの代替として注目されています。

https://supabase.com/

# Supabaseでプロジェクトを作成しよう

アカウントを作成すると、`New project`から新しいプロジェクトを作成できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2823873/76c402e1-2ca2-518f-7824-433ab5d66d1c.png)

作成するとこんな感じ。

次に左のメニューからSQL Editorを開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2823873/6435ed08-54c4-ff16-be4b-3543c158c74e.png)

ここにSQLを書くことで、テーブルを作成できます。

今回はサンプルとして、usersテーブルとscoresテーブルを作り、1対多の関係を持たせます。

```sql
create table public.users (
  id uuid default gen_random_uuid() primary key,
  name text not null,
  created_at timestamp with time zone default now()
);
```

```sql
create table public.scores (
  id uuid default gen_random_uuid() primary key,
  user_id uuid references public.users (id) on delete cascade not null,
  value int not null,
  created_at timestamp with time zone default now()
);
```

実行が成功すると画面下部に「Success. No rows returned」と表示されます。

![スクリーンショット 2024-12-23 19.35.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2823873/206ba0f3-6a87-6b16-d679-16353879c384.png)

次に、それぞれのテーブルにテストデータを追加しましょう。

```sql
-- Insert test data into the 'users' table
INSERT INTO users (id, name, created_at)
VALUES
    ('11111111-1111-1111-1111-111111111111', 'Alice', NOW()),
    ('22222222-2222-2222-2222-222222222222', 'Bob', NOW()),
    ('33333333-3333-3333-3333-333333333333', 'Charlie', NOW());

-- Insert test data into the 'scores' table
INSERT INTO scores (id, user_id, value, created_at)
VALUES
    ('44444444-4444-4444-4444-444444444444', '11111111-1111-1111-1111-111111111111', 100, NOW()),
    ('55555555-5555-5555-5555-555555555555', '11111111-1111-1111-1111-111111111111', 150, NOW()),
    ('66666666-6666-6666-6666-666666666666', '22222222-2222-2222-2222-222222222222', 200, NOW()),
    ('77777777-7777-7777-7777-777777777777', '33333333-3333-3333-3333-333333333333', 50, NOW()),
    ('88888888-8888-8888-8888-888888888888', '33333333-3333-3333-3333-333333333333', 75, NOW());
```

左のメニューからDatabase > Schema Visualizerを選ぶと、現在のテーブルの関係が見られます。

![スクリーンショット 2024-12-23 19.37.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2823873/eab77b20-f547-a62c-99fe-bb3ecc928bcc.png)

また、Table Editorから現在のデータ一覧が確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2823873/1bbafdda-6c89-6302-b9e9-45c6142ef80e.png)

# Supabaseをアプリに組み込もう

次に、Supabaseを用いてアプリを作成していきます。

## Swift Package ManagerでSupabaseを追加

まずはSwift Package ManagerでSupabaseを追加します。

https://github.com/supabase/supabase-swift

![スクリーンショット 2024-12-23 20.29.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2823873/2cabded3-b3db-97b2-4b5e-b5e0f07c6a1c.png)

ここで、`import Supabase`がうまく反応しない場合は、ライブラリとしてSupabaseが認識されていない場合があります。

![スクリーンショット 2024-12-23 20.11.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2823873/2a8eec60-d7e5-51c2-3af2-c6a15fb2def0.png)

Frameworks, Libraries, and Embedded Contentの+ボタンから、Supabaseを追加します。

![スクリーンショット 2024-12-23 20.11.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2823873/77319e14-ab8e-ae1e-9be5-edaf337e67aa.png)

## コードを書く

次にコードを書いていきます。

```swift:Models.swift
// Models.swift
import Foundation

struct User: Identifiable, Decodable {
    let id: UUID
    let name: String
    let created_at: Date
}

struct Score: Identifiable, Decodable {
    let id: UUID
    let user_id: UUID
    let value: Int
    let created_at: Date
}
```

```swift:SupabaseManager
import Foundation
import Supabase

class SupabaseManager {
    static let shared = SupabaseManager()
    
    let client: SupabaseClient
    
    private init() {
        // Supabase URLとAnon Keyで置き換え
        let url = URL(string: "https://your url.supabase.co")!
        let key = "Your API KEY"
        client = SupabaseClient(supabaseURL: url, supabaseKey: key)
    }
}

```

ここで、Supabase URLとAnon Keyは画面右下のProject Setting > APIから確認できます。

![スクリーンショット 2024-12-23 20.33.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2823873/cf26f820-0f68-50b6-0e3b-3da6b7df3a6b.png)

```swift:ContentView.swift
// ContentView.swift
import SwiftUI

struct ContentView: View {
    @StateObject private var viewModel = UserListViewModel()
    
    var body: some View {
        NavigationView {
            List(viewModel.users) { user in
                NavigationLink(destination: ScoreListView(user: user)) {
                    Text(user.name)
                }
            }
            .navigationTitle("Users")
            .task {
                await viewModel.fetchUsers()
            }
        }
    }
}
```

```swift:UserListViewModel.swift
// UserListViewModel.swift
import Foundation
import Supabase

@MainActor
class UserListViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var error: String?
    
    func fetchUsers() async {
        do {
            let response: PostgrestResponse<[User]> = try await SupabaseManager.shared.client
                .from("users")
                .select()
                .execute()
            
            users = response.value
        } catch {
            self.error = error.localizedDescription
        }
    }
}
```

```swift:ScoreListView.swift
// ScoreListView.swift
import SwiftUI

struct ScoreListView: View {
    let user: User
    @StateObject private var viewModel = ScoreListViewModel()
    
    var body: some View {
        List(viewModel.scores) { score in
            HStack {
                Text("Score: \(score.value)")
                Spacer()
                Text("\(score.created_at, formatter: dateFormatter)")
                    .font(.caption)
                    .foregroundColor(.gray)
            }
        }
        .navigationTitle("\(user.name)'s Scores")
        .task {
            await viewModel.fetchScores(for: user.id)
        }
    }
}

let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .short
    formatter.timeStyle = .short
    return formatter
}()
```

```swift:ScoreListViewModel.swift
// ScoreListViewModel.swift
import Foundation
import Supabase

@MainActor
class ScoreListViewModel: ObservableObject {
    @Published var scores: [Score] = []
    @Published var error: String?
    
    func fetchScores(for userId: UUID) async {
        do {
            let response: PostgrestResponse<[Score]> = try await SupabaseManager.shared.client
                .from("scores")
                .select()
                .eq("user_id", value: userId)
                .execute()
            
            scores = response.value
        } catch {
            self.error = error.localizedDescription
        }
    }
}
```

これで実行すると、ちゃんとデータを取得することができます。

![simulator_screenshot_E561FDE5-ECC4-40D7-8775-C3FEE4AC1853.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2823873/66de1ed3-3170-7d1d-a035-27d4565bfca9.png)

# まとめ

GraphQLなどもサポートされていてかなり使い勝手が良い印象を受けるので、これからも使い倒していきたいなと思います！皆さんも使っていきましょう！