突然のお達し
いきなりですが、みなさんは上長などにクロスプラットフォーム勉強してよ
と言われたことはありませんか?
かく言う私は、今勤めている会社の社長から直々に言われてしまいました。
でも、FlutterやReactNativeを触る意欲が湧かないんだよなぁ...
(DartでUI書くとネストが深くなりがちだし、(個人的な感想です)
ReactNativeはTypeScriptが個人的に好きじゃない...
linter突っ込んでもeslint ignore やbiome ignore, tsignore など好き勝手にされた挙句、心が折れた Any (個人的なトラウマです) Any
僕はSwiftで開発したいの...
という時に見つかったのがSkip Toolsです
Skip Toolsとは
Swiftで書いたコードをビルド時にkotlinへトランスパイルしてくれる優れ者
今年の8月ごろにversion 1.0.0が正式リリース
※ 2024/12/15現在の最新バージョンは1.2.8
それなりに開発が活発であり、どんなもんなのかなぁ〜と試しに手を動かして試してみることとしました
環境セットアップ
公式Docに環境設定方法が記載されているので、こちらを参照
基本的な流れは
・brew経由でskipをインストール
・Android Studioインストール
・ライセンスキーを設定
・CLIからプロジェクトを作成
になります。
今回はこちらについては詳しく記載しません
(確か、他にも設定方法を記載している人はいたと思うので
いざ、開発
と、言っても特に作りたいな〜ってものが思い浮かばなかったので普段お世話になっているSupabaseに繋げてみることにしました
※ Supabaseのローカル開発環境セットアップは省略
最初はsupabase-swiftとsupabase-ktの両方を入れて、コード上でこねくり回してsupabase連携するのかな?
と思ったのですが、公式がライブラリを提供していました
※ ちなみにSkip ToolsはSwiftのコード上にkotlin codeを組み込んだり、java kotlinライブラリを導入することもできます(ネイティブコードに直接アクセスできるのは嬉しい限り
Swift Package導入
と、ここで第一関門、Packageの導入
普段はXcodeのPackage Dependenciesから雑に突っ込むだけでいけてましたが、Skipはどうやらそのようにはいかないみたいです
project root/project name/Package.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の解決を実施
cd path/to/root/project-name
swift package resolve
supabase signIn
ここまで終わったら
supabaseのアクセス情報とsignIn処理を書いて
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)
}
}
あとはログイン画面を雑に作って、ログインできたら画面遷移するような処理を書く
(めんどくさいので省略
Android Login | Android Logined |
iOS Login | iOS Logined |
Table操作
続いて、テーブル操作を見ていきます
開発者に尋ねたところ、Testを見ればわかるようになっているよ
とのことでした
select
let countryCount0: PostgrestResponse<Void> = try await client
.from(table)
.select(count: CountOption.exact)
.execute()
Insert
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
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"),
],
そして二、三日悩む...(うーん。難しいなぁ〜
kotlin compile時の挙動やsupabase-swift, supabase-ktの仕様の違いに苦労し、妥協を重ね
rpcのparameterにはDictionaryを指定することで運用回避的な対策
動くしまぁいいか()というダメダメ思想で PR作成
(色々あったんですよ、compilerの仕組みが違うからGenericsの型情報消えたり。ここに書くと長文になりそうだから一旦割愛)
無事マージしてリリースされました
で、実際の呼び出しはこんな感じ
受け渡す際は全部Stringにして
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に型変換してる
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で作られてきた資産を使い回すことができること
まだまだ、全てをワンソースで書き切るという段階には至っていなかったですが、今後の成長に期待したいな!と思わせるツールでした(現状、仕事で使うなら筋肉が必要。笑)