Skip toolsを使ったSwiftベースのクロスプラットフォーム開発 with Supabase

## 突然のお達し

いきなりですが、みなさんは上長などにクロスプラットフォーム勉強してよ
と言われたことはありませんか？

かく言う私は、今勤めている会社の社長から直々に言われてしまいました。

でも、FlutterやReactNativeを触る意欲が湧かないんだよなぁ...

~~（DartでUI書くとネストが深くなりがちだし、(個人的な感想です)~~
~~ReactNativeはTypeScriptが個人的に好きじゃない...~~
~~linter突っ込んでもeslint ignore やbiome ignore, tsignore など好き勝手にされた挙句、心が折れた Any (個人的なトラウマです) Any~~

僕はSwiftで開発したいの...

という時に見つかったのがSkip Toolsです

## Skip Toolsとは

https://skip.tools/

Swiftで書いたコードをビルド時にkotlinへトランスパイルしてくれる優れ者

今年の8月ごろにversion 1.0.0が正式リリース
※ 2024/12/15現在の最新バージョンは1.2.8

https://skip.tools/blog/skip-1_0-release/

それなりに開発が活発であり、どんなもんなのかなぁ〜と試しに手を動かして試してみることとしました

## 環境セットアップ

公式Docに環境設定方法が記載されているので、こちらを参照

https://skip.tools/docs/gettingstarted/

基本的な流れは

・brew経由でskipをインストール
・Android Studioインストール
・ライセンスキーを設定
・CLIからプロジェクトを作成

になります。
今回はこちらについては詳しく記載しません

（確か、他にも設定方法を記載している人はいたと思うので

## いざ、開発

と、言っても特に作りたいな〜ってものが思い浮かばなかったので普段お世話になっているSupabaseに繋げてみることにしました

https://supabase.com/

※ Supabaseのローカル開発環境セットアップは省略

最初はsupabase-swiftとsupabase-ktの両方を入れて、コード上でこねくり回してsupabase連携するのかな？
と思ったのですが、公式がライブラリを提供していました

https://github.com/skiptools/skip-supabase

※ ちなみにSkip ToolsはSwiftのコード上にkotlin codeを組み込んだり、java kotlinライブラリを導入することもできます（ネイティブコードに直接アクセスできるのは嬉しい限り

https://skip.tools/docs/dependencies/#implementation

## Swift Package導入

と、ここで第一関門、Packageの導入

普段はXcodeのPackage Dependenciesから雑に突っ込むだけでいけてましたが、Skipはどうやらそのようにはいかないみたいです

project root/project name/Package.swiftに直接記述して

```swift
let package = Package(
    ... 省略
    dependencies: [
        .package(url: "https://source.skip.tools/skip.git", from: "1.0.0"),
        .package(url: "https://source.skip.tools/skip-ui.git", from: "1.0.0"),
        // git urlとversionを指定
        .package(url: "https://source.skip.tools/skip-supabase.git", from: "0.0.5"),
    ],
    targets: [
        .target(
            name: "ProjectName",
            dependencies: [
                .product(name: "SkipUI", package: "skip-ui"),
                // targetの追加
                .package(url: "https://source.skip.tools/skip-supabase.git", from: "0.0.5"),
            ],
            resources: [.process("Resources")],
            plugins: [.plugin(name: "skipstone", package: "skip")]
        ),
        ... 省略
    ]
)
```

terminalからswiftコマンドを実行してpackageの解決を実施

```zsh
cd path/to/root/project-name
swift package resolve
```

## supabase signIn

ここまで終わったら
supabaseのアクセス情報とsignIn処理を書いて

```swift
import Foundation
import SkipSupabase

#if SKIP
let debugUrl = "http://10.0.2.2:54321"
#else
let debugUrl = "http://127.0.0.1:54321"
#endif

fileprivate let client = SupabaseClient(
    supabaseURL:
        URL(string: debugUrl)!,
    supabaseKey: "anon-key"
)

struct MySupabaseClient {
    func signIn(user: User) async throws {
        let ac: AuthClient = client.auth
        try await ac.signIn(email: user.mailAddress, password: user.password)
    }
}
```

https://supabase.com/docs/reference/swift/auth-signinwithpassword

あとはログイン画面を雑に作って、ログインできたら画面遷移するような処理を書く
（~~めんどくさいので省略~~

|![Screenshot_1734226377.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3961356/08a073da-161a-a89e-b33e-68a3f483dea7.png)| ![Screenshot_1734226413.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3961356/a8accd3e-43bb-2781-06ef-5a9c811ecf84.png) |
| --- | --- |
| Android Login | Android Logined |

|![Simulator Screenshot - iPhone 16 Pro Max - 2024-12-15 at 10.32.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3961356/da6d9d36-bfc4-1463-4d2a-051ffbeea86a.png)|![Simulator Screenshot - iPhone 16 Pro Max - 2024-12-15 at 10.33.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3961356/de9e6222-0787-57e6-7f19-ac0e00ad7441.png)|
| --- | --- |
| iOS Login | iOS Logined |


## Table操作

続いて、テーブル操作を見ていきます

開発者に尋ねたところ、Testを見ればわかるようになっているよ
とのことでした

https://github.com/skiptools/skip-supabase/blob/main/Tests/SkipSupabaseTests/SkipSupabaseTests.swift

### select

```swift
let countryCount0: PostgrestResponse<Void> = try await client
    .from(table)
    .select(count: CountOption.exact)
    .execute()
```

### Insert

```swift
let icountryResponse: PostgrestResponse<[Country]> = try await client
    .from("countries")
    .insert(Country(id: 1, name: "USA"), returning: PostgrestReturningOptions.representation)
    // .single() // TODO: handle single results
    .execute(options: FetchOptions(head: false, count: CountOption.exact))
```

### delete

```
let voidResponse: PostgrestResponse<Void> = try await client
    .from("countries")
    .delete()
    .gte("id", value: 0)
    .execute()
```

### update

```
try await client
    .from("countries")
    .update(["name": "Australia"])
    .eq("id", value: 1)
    .execute()
```

## 第二関門 rpc functionが呼び出せない...

どうやら、rpc functionがサポートされていませんでした

ってことでContributeしていきましょう

skip supabaseをローカルに落として、local packageとして読み込ませる

### Package.swift

```swift
dependencies: [
        .package(url: "https://source.skip.tools/skip.git", from: "1.0.0"),
        .package(url: "https://source.skip.tools/skip-ui.git", from: "1.0.0"),
        .package(path: "path/to/root/skip-supabase/"),
        // .package(url: "https://source.skip.tools/skip-supabase.git", from: "0.0.5"),
    ],
```

![スクリーンショット 2024-12-15 11.00.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3961356/398076fc-0aa0-54d8-1f65-884947d8b8c4.png)

そして二、三日悩む...（うーん。難しいなぁ〜

kotlin compile時の挙動やsupabase-swift, supabase-ktの仕様の違いに苦労し、妥協を重ね

rpcのparameterにはDictionary<String, String>を指定することで運用回避的な対策
動くしまぁいいか()というダメダメ思想で PR作成
(色々あったんですよ、compilerの仕組みが違うからGenericsの型情報消えたり。ここに書くと長文になりそうだから一旦割愛)

![スクリーンショット 2024-12-15 10.55.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3961356/aef3dd5f-c40b-ed3f-5957-31e9d02209ba.png)

無事マージしてリリースされました

で、実際の呼び出しはこんな感じ

受け渡す際は全部Stringにして

```swift
    func register(title: String, startDate: Date, endDate: Date, details: [ExampleDTO], expenses: [RegisterExpenseDTO]) async throws {
        let detailsJsonData = try! JSONEncoder().encode(details)
        let detailsJsonString = String(data: detailsJsonData, encoding: .utf8)!
        
        let _ =
        try await client
            .rpc(
                "insert_education_plan",
                params: [
                    "plan_name_input": title,
                    "start_date_input": startDate.format(style: "YYYY/MM/dd"),
                    "end_date_input": endDate.format(style: "YYYY/MM/dd"),
                    "details_input": detailsJsonString
                ]
            )
            .execute()
    }

```


postgre function側で受け取る際に適宜型情報を調整
...JSONはうまく行かなかったのでtextで受け取って、jsonに型変換してる


```sql
CREATE OR REPLACE FUNCTION public.insert_example(
    plan_name_input text,
    start_date_input date,
    end_date_input date,
    details_input text,  -- 文字列として受け取る
) 
RETURNS VOID 
LANGUAGE plpgsql 
AS $function$
DECLARE 
    expenses_jsonb jsonb;  -- jsonbに変換したもの
BEGIN 
    -- 文字列をjsonbにキャスト
    details_jsonb := details_input::jsonb;
```

supabase edge functionとかも対応していないので、時間があったらcontributeしてください🙇（僕が喜びます）

## 感想

個人的にはSwift + SwiftUIでAndroidも作れることや、困ったらkotlinのコードを記載できるのでアリだと思います
（途中で添付したカレンダーも実はkotlin + kotlinライブラリで作ったものだったりします

SwiftUIでかける基本的なものはサポートされているし、swiftやkotlin, javaで作られてきた資産を使い回すことができること

まだまだ、全てをワンソースで書き切るという段階には至っていなかったですが、今後の成長に期待したいな！と思わせるツールでした（現状、仕事で使うなら筋肉が必要。笑）